Objective-C的哪些特性利用了运行时机制

这是我遇到的一道面试题。虽然当时回答得一般,但是我很喜欢这道题目,因为它可以考察出面试者对运行时机制理解的深度和广度,所以这里想试着重新回答一下。

现在的回答可能还很浅薄,如果以后想到了什么能补充的地方也会更新这篇( ̄▽ ̄)

本文中关于runtime的源码参考自苹果开源的objc4-680


Method Swizzling



在一些静态语言比如C语言中,调用一个函数后会跳到哪个地址执行哪些指令,是在编译链接的时候确定的。而在Objective-C中,向一个对象发送消息时,具体会执行哪个方法,则是运行时系统根据selector查找对应的IMP得到的。

所以通过交换两个selector对应的IMP,可以完成对执行的具体方法的调换。


Associated Object



使用

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object)

这三个方法,可以在运行时为一个对象建立一个关联对象。程序员可以为现有的类增添关联对象,起到类似于动态添加“实例变量”的作用。

可以在苹果开源的runtime代码中找到这几个方法的实现。比如在objc-references.mm中可以找到objc_setAssociatedObject最终调用的方法:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) 

阅读这几个方法的实现,可以大概了解到关联对象的实现机制。它维护了一个全局的AssociationsHashMap对象。这是一个map对象,其中键为被关联对象的地址,值为一个指向ObjectAssociationMap对象的指针。而ObjectAssociationMap类派生自std::map,键为传入的“key”参数(是一个地址),值为一个指向ObjcAssociation对象的指针。最后,ObjcAssociation中的成员变量_value指的就是那个关联对象。


Category



在Objective-C中,因为一个类中有哪些方法是在运行时决定的,所以可以用category给任何类增加方法。

objc-runtime-new.mm中,可以看到methodizeClass方法

/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void methodizeClass(Class cls)
{

......

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);

........

}

根据注释,这个方法负责将一个类的方法列表、协议列表和属性列表整理好。在这个类的实现中,我们可以看到,相比于整理类自身的方法列表、协议列表和属性列表,将category中的内容关联到对应的类上,是之后做的事情。

查看attachCategories的方法实现,其中,category中的方法列表是这样关联到对应的类中的:

rw->methods.attachLists(mlists, mcount);

在category将方法列表关联到类的方法列表的过程中,是不会判断selector是否重名的。如果类A中已经定义了方法demoMethod,那么在A的category中定义重名的demoMethod,会导致加载完成后,类A的方法列表中有两个名为demoMethod的方法。可以做个小实验验证:

unsigned int count = 0;
Method *methodList = class_copyMethodList([A class], &count);
Method *methodHeader = methodList;
while (count--) {
    Method method = *methodHeader;
    SEL selector = method_getName(method);
    NSLog(@"sel: %@", NSStringFromSelector(selector));
    methodHeader++;
}
free(methodList);

这段代码的输出:

2016-06-26 18:31:04.251 XSQCategoryDemo[3410:5607532] sel: demoMethod
2016-06-26 18:31:04.252 XSQCategoryDemo[3410:5607532] sel: demoMethod

即表示,类A的方法列表中有两个名为demoMethod的方法。


KVO



KVO的实现原理是isa-swizzling

官方文档中解释了:当一个观察者被注册到一个对象上的时候,这个对象的isa指针会被更改,指向一个中间类而非真正的类。

如果做实验验证一下:

A *a = [[A alloc] init];
NSLog(@"%@", object_getClass(a));
[a addObserver:someObserver forKeyPath:@"someKeyPath" options:0 context:nil];
NSLog(@"%@", object_getClass(a));

这一小段代码的输出是:

2016-06-25 23:56:49.381 XSQKVODemo[88055:5252219] A
2016-06-25 23:56:52.161 XSQKVODemo[88055:5252219] NSKVONotifying_A

可以看出,在注册了观察者之后,对象指向的类发生了改变。


KVC



在Foundation的NSKeyValueCoding.h中,几个KVO方法都用详细的注释介绍了它的搜索策略。比如

- (nullable id)valueForKey:(NSString *)key;

方法是这样执行的:

  1. 搜索消息接收者所在类的accessor方法,依次寻找名字能匹配
    -get<Key>-<key>或者-is<Key>的方法,如果找到则调用;
  2. 搜索消息接收者所在类的方法,如果有能匹配NSOrderedSet的方法,则会返回一个代理对象,这个代理对象可以接收所有NSOrderedSet对象可以接收的消息;
  3. 搜索消息接收者所在类的方法,如果能匹配NSArray的方法,则返回一个可以接收所有NSArray对象可以接收的消息的代理对象;
  4. 搜索消息接收者所在类的方法,如果能匹配NSSet的方法,则返回一个可以接收所有NSSet对象可以接收的消息的代理对象;
  5. 如果消息接收者所在的类的+accessInstanceVariablesDirectly方法返回的是YES,就依次寻找名字能匹配
    _<key>_is<Key><key>或者is<Key>的实例变量;
  6. 最后,调用-valueForUndefinedKey:然后返回结果。

虽然没找到明确的文档说KVC利用了运行时机制,但类似这样对方法名、对实例变量的搜索,没有运行时系统的支持应该是无法做到的。

其实如果在Xcode中设置一个符号断点class_getInstanceMethod,也可以调试到这个方法被KVC的一些方法调用了:


参考

objc4-680
Objective-C Runtime Reference
Key-Value Observing Implementation Details
刨根问底Objective-C Runtime(3)- 消息 和 Category
iOS 程序 main 函数之前发生了什么
Objective-C Associated Objects 的实现原理
objc category的秘密
Key-Value Coding and Observing
深入理解Objective-C:Category

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容