美文网首页iOS_summary
Objective-C 的底层实现

Objective-C 的底层实现

作者: 忻凯同学 | 来源:发表于2018-01-22 17:03 被阅读47次

    前言

            在过去的几年中,涌现了大量的Objective-C开发者(当然我也是其中一名,还是个小白而已)。有些是从动态语言转过来的,比如Ruby或Python;有些是从强类型语言转过来的,如 Java或C#;当然也有直接以Objective-C作为入门语言的。也就是说有很大一部分开发者都没有使用Objective-C太长时间。当你接触一门新语言时,更多地会关注基础知识,如语法和特性等。但通常有一些更高级的,更鲜为人知又有强大功能的特性等待你去开拓。

            这篇文章主要是来领略下Objective-C的运行时(runtime),同时解释是什么让Objective-C如此动态,然后感受下这些动态化的技术细节。希望这回让你对Objective-C和Cocoa是如何运行的有更好的了解。文章如有不妥之处,还请及时指出。


    一. The Runtime(运行时)

            Objective-C是一门简单的语言,95%是C,只是在语言层面上加了些关键字和语法。真正让Objective-C如此强大的是它的运行时,它很小但却很强大,它的核心是消息分发。

    1. Messages(消息)

            如果你是从动态语言如Ruby或Python转过来的,可能知道什么是消息,可以直接跳过进入下一节。如果是从其他语言转过来的,请继续看。

            在执行一个方法时,有些语言,编译器会执行一些额外的优化和错误检查,因为调用关系很直接也很明显。但对于消息分发来说,就不那么明显了。在发消息前不必知道某个对象是否能够处理消息。你把消息发给它,它可能会处理,也可能转给其他的Object来处理。一个消息不必对应一个方法,一个对象可能实现一个方法来处理多条消息。

            在Objective-C中,消息是通过objc_msgSend()这个runtime方法及相近的方法来实现的。这个方法需要一个target(对象),selector(方法选择器),还有一些操作参数。理论上来说,编译器只是把消息分发变成objc_msgSend来执行。

            比如下面这两行代码是等价的。

    [array insertObject:foo atIndex:5];

    objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

    2. Objects, Classes, MetaClasses

            在大多数面向对象的语言里有 classes(类) 和 objects(对象) 的概念,Objects通过Classes生成。但是在Objective-C中,classes本身也是objects(这点跟python很像),也可以处理消息,这也是为什么会有类方法和实例方法。

            具体来说,Objective-C中的Object是一个结构体(struct),第一个成员是isa,指向自己的class。这是在objc/objc.h中定义的。

    typedef struct objc_object {

            Class _Nonnull isa OBJC_ISA_AVAILABILITY;

    } *id;

            object的class保存了方法列表,还有指向父类的指针。但classes也是objects,也会有isa变量,那么它又指向哪儿呢?这里就引出了第三个类型: metaclasses(元类)。一个 metaclass被指向class,class被指向object。它保存了所有实现的方法列表,以及父类的metaclass。

            如果想更清楚地了解objects,classes以及metaclasses的关系及其如何一起工作的,可以阅读这篇文章

    3. Methods, Selectors and IMPs

            我们知道了运行时会发消息给对象,我们也知道一个对象的class保存了方法列表。那么这些消息是如何映射到方法的,这些方法又是如何被执行的呢?

            第一个问题的答案很简单。class的方法列表其实是一个字典,key为selectors,IMPs为value。一个IMP是指向方法在内存中的实现。很重要的一点是,selector和IMP之间的关系是在运行时才决定的,而不是编译时。

            IMP通常是指向方法的指针,第一个参数是self,类型为id,第二个参数是_cmd,类型为SEL,余下的是方法的参数。这也是self和_cmd被定义的地方。

            下面演示了Method和IMP的不同:

    - (id)doSomethingWithInt:(int)aInt{}

    id doSomethingWithInt(id self, SEL _cmd, int aInt){}

    4. 其他运行时方法

            我们在了解了objects,classes,selectors,IMPs以及消息分发之后,那么运行时到底能做什么呢?主要有两个作用:

            1> 创建、修改、自省classes和objects

            2> 消息分发

            之前已经提过消息分发,不过只是一小部分功能,所有的运行时方法都有特定的前缀,下面是一些其他的属性或方法:

    4.1 class

            class开头的方法是用来修改和自省classes。

            1> 允许重建classes:class_addIvar、class_addMethod、class_addProperty 和 class_addProtocol 

            2> 拿到一个class的所有内容:class_copyIvarList、class_copyMethodList、class_copyProtocolList 和class_copyPropertyList。

            3> 返回单个内容:class_getClassMethod、class_getClassVariable、class_getInstanceMethod、 class_getInstanceVariable、class_getMethodImplementation 和 class_getProperty。

            4> 也有一些通用的自省方法:class_conformsToProtocol、class_respondsToSelector、 class_getSuperclass。

            5> 创建一个object:class_createInstance。

    4.2 ivar

            ivar的方法能让你得到名字,内存地址和Objective-C type encoding。

    4.3 method

            1> 用来自省:method_getName、method_getImplementation、method_getReturnType等等。

            2> 修改的方法:method_setImplementation 和 method_exchangeImplementations。

    4.4 objc

            一旦拿到了object,你就可以对它做一些自省和修改。

            你可以get/set ivar,使用 object_copy 和 object_dispose 来 copy 和 free object 的内存。不仅拿到一个class,还可以使用 object_setClass 来改变一个 object 的 class。

    4.5 property

            属性保存了很大一部分信息。

            除了拿到名字,你还可以使用property_getAttributes来发现property的更多信息,如返回值、是否为atomic、getter/setter名字、是否为dynamic、背后使用的ivar名字、是否为弱引用等。

    4.6 protocol

            Protocols有点像classes,但是精简版的,运行时的方法是一样的。你可以获取method,property,protocol列表,检查是否实现了其他的protocol。

    4.7 SEL

            Objective-C有一些方法可以处理 selectors,比如获取名字,注册一个selector等等。


    二. Classes And Selectors From Strings(反射机制)

    1. 反射机制

            比较基础的一个动态特性是反射机制,通过String来生成Classes和Selectors。

            Cocoa提供了NSClassFromString和NSSelectorFromString方法,使用起来很简单:

    SEL selector = NSSelectorFromString(@"setImageViewWith:");

    Class stringclass = NSClassFromString(@"NSString");

            接下来:

    NSString *myString = [stringclass stringWithString:@"Hello World"];

            为什么要这么做呢?通常情况下,直接使用Class不是很方便,但有些场景下这个方法会很有用。首先,可以得知是否存在某个class,NSClassFromString会返回nil,如果运行时不存在该class的话。比如可以检查NSClassFromString(@"NSRegularExpression")是否为nil来判断是否为iOS4.0+。

    2. 参数决定返回值的类型

            根据不同的参数返回不同数据类型:class或method。

            比如你在解析一些数据,每个数据项都有要解析的字符串以及自身的类型(String,Number,Array)。你可以在一个方法里搞定这些,也可以使用多个方法。

            1> 获取type,然后使用if来调用匹配的方法。

            2> 根据type来生成一个selector,然后调用之。

            以下是两种实现方式:

    - (void)parseObject:(id)object {

            for (id data in object) {

                    if ([[data type] isEqualToString:@"String"]) {

                            [self parseString:[data value]];

                    } else if ([[data type] isEqualToString:@"Number"]) {

                            [self parseNumber:[data value]];

                    } else if ([[data type] isEqualToString:@"Array"]) {

                            [self parseArray:[data value]];

                    }

            }

    }

    - (void)parseObjectDynamic:(id)object {

            for (id data in object) {

                    [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"parse%@:", [data type]]) withObject:[data value]];

            }

    }

    - (void)parseString:(NSString *)aString {}

    - (void)parseNumber:(NSString *)aNumber {}

    - (void)parseArray:(NSString *)aArray {}

            由此可见,第二种方法显然简介,把7行带if的代码变成1行,这样的好处就是,如果将来有新的类型,只需增加实现方法即可,而不用再去添加新的 else if。


    三. Method Swizzling

            上述说过,方法由两个部分组成。Selector相当于一个方法的id;IMP是方法的实现。这样分开的一个便利之处是selector和IMP之间的对应关系可以被改变。比如一个 IMP 可以有多个 selectors 指向它。

            而 Method Swizzling 可以交换两个方法的实现,那什么情况下会需要这个呢?先来看一下Objective-C中的两种扩展class的途径:

            1> 首先是 subclassing。你可以重写某个方法,调用父类的实现,这也意味着你必须使用这个subclass的实例。

            2> 如果继承了某个Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)。这种情况下,你会想添加一个方法到NSArray,也就是使用Category。大多数情况下这是可以实现的,但如果你重写了某个方法,就没有机会再调用原先的实现了。

            Method Swizzling 可以搞定这个问题。你可以重写某个方法而不用继承,同时还可以调用原先的方法。通常的做法是在category中添加一个方法(当然也可以是一个全新的class)。可以通过method_exchangeImplementations这个运行时方法来交换实现。来看一个demo,这个demo演示了如何重写addObject: 方法来纪录每一个新添加的对象。

    #import <objc/runtime.h>

    @interface NSMutableArray (LoggingAddObject)

    - (void)logAddObject:(id)aObject;

    @end

    @implementation NSMutableArray (LoggingAddObject)  

    + (void)load {

            Method addObject = class_getInstanceMethod(self, @selector(addObject:));

            Method logAddObject = class_getInstanceMethod(self, @selector(logAddObject:));

            method_exchangeImplementations(addObject, logAddObject);

    }  

    - (void)logAddObject:(id)aobject {

            [self logAddObject:aObject];    

            NSLog(@"Added object %@ to array %@", aObject, self);

    }  

    @end

            我们把方法交换放到了load中,这个方法只会被调用一次,而且是运行时载入。如果指向临时用一下,可以放到别的地方。注意到一个很明显的递归调用logAddObject:。这也是Method Swizzling容易把我们搞混的地方,因为我们已经交换了方法的实现,所以其实调用的是addObject:。

    Method Swizzling关系图

    四. 动态继承、交换

            我们可以在运行时创建新的class,这个特性用得不多,但其实它还是很强大的。你能通过它创建新的子类,并添加新的方法。

            但这样的一个子类有什么用呢?别忘了Objective-C的一个关键点:object内部有一个叫做isa的变量指向它的class。这个变量可以被改变,而不需要重新创建。然后就可以添加新的ivar和方法了。可以通过以下命令来修改一个object的class.

    object_setClass(myObject, [MySubclass class]);

            这可以用在Key Value Observing。当你开始对一个对象的某种属性进行观察时,Cocoa会创建这个对象的派生类,然后将这个对象的isa指向新创建的派生类。点击这里查看更详细的解释。


    五. 动态方法处理

            目前为止,我们讨论了方法交换,以及已有方法的处理。那么当你发送了一个object无法处理的消息时会发生什么呢?很明显,"it breaks"。大多数情况下确实如此,但Cocoa和runtime也提供了一些应对方法。

            首先是动态方法处理。通常来说,处理一个方法,运行时寻找匹配的selector然后执行之。有时,你只想在运行时才创建某个方法,比如有些信息只有在运行时才能得到。要实现这个效果,你需要重写+resolveInstanceMethod: 或 +resolveClassMethod:。如果确实增加了一个方法,记得返回YES。

    + (BOOL)resolveInstanceMethod:(SEL)aSelector {

            if (aSelector == @selector(myDynamicMethod)) {

                    class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:");

                    return YES;

            }

            return [super resolveInstanceMethod:aSelector];

    }

            那Cocoa在什么场景下会使用这些方法呢?Core Data用得很多。NSManagedObjects有许多在运行时添加的属性用来处理get/set属性和关系。


    六. 消息转发

            如果 resolve method 返回NO,运行时就进入下一步骤:消息转发。

            有两种常见用例:

            1> 将消息转发到另一个可以处理该消息的object。

            2> 将多个消息转发到同一个方法。

            消息转发分两步。首先,运行时调用-forwardingTargetForSelector:,如果只是想把消息发送到另一个object,那么就使用这个方法,因为更高效。如果想要修改消息,那么就要使用-forwardInvocation:,运行时将消息打包成NSInvocation,然后返回给你处理。处理完之后,调用invokeWithTarget:。

            Cocoa有几处地方用到了消息转发,主要的两个地方是代理(Proxies)和响应链(Responder Chain)。

            1> NSProxy是一个轻量级的class,它的作用就是转发消息到另一个object。如果想要惰性加载object的某个属性会很有用。NSUndoManager也有用到,不过是截取消息,之后再执行,而不是转发到其他的地方。

            2> 响应链是关于Cocoa如何处理与发送事件与行为到对应的对象。首先是First Responder,通常是当前的UI。如果没有处理该消息,则转发到下一个-nextResponder。这么一直下去直到找到能够处理该消息的object,或者没有找到,报错。


    七. 使用Block作为Method IMP

            在iOS 4.3之后,有了很多新的runtime方法。除了对properties和protocols的加强,还带来一组新的以 imp 开头的方法。通常一个 IMP 是一个指向方法实现的指针,头两个参数为 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 带来了block,imp_implementationWithBlock()能让我们使用block作为 IMP,API如下

    IMP imp_implementationWithBlock(id block);

    下面这个代码片段展示了如何使用block来添加新的方法。

    IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {

            NSLog(@"Hello %@", string);

    });

    class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@");

    如果想知道这是如何实现的,可以查看这篇文章

            可以看到,Objective-C 表面看起来挺简单,但还是很灵活的,可以带来很多可能性。动态语言的优势在于在不扩展语言本身的情况下做很多很灵巧的事情。比如Key Value Observing,提供了优雅的API可以与已有的代码无缝结合,而不需要新增语言级别的特性。

            希望这篇文章能让你更深入地了解Objective-C,在开发app时也能开阔思路,考虑更多的可能性。

    相关文章

      网友评论

        本文标题:Objective-C 的底层实现

        本文链接:https://www.haomeiwen.com/subject/kbsooxtx.html