这是我遇到的一道面试题。虽然当时回答得一般,但是我很喜欢这道题目,因为它可以考察出面试者对运行时机制理解的深度和广度,所以这里想试着重新回答一下。
现在的回答可能还很浅薄,如果以后想到了什么能补充的地方也会更新这篇( ̄▽ ̄)
本文中关于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;
方法是这样执行的:
- 搜索消息接收者所在类的accessor方法,依次寻找名字能匹配
-get<Key>
、-<key>
或者-is<Key>
的方法,如果找到则调用; - 搜索消息接收者所在类的方法,如果有能匹配
NSOrderedSet
的方法,则会返回一个代理对象,这个代理对象可以接收所有NSOrderedSet
对象可以接收的消息; - 搜索消息接收者所在类的方法,如果能匹配
NSArray
的方法,则返回一个可以接收所有NSArray
对象可以接收的消息的代理对象; - 搜索消息接收者所在类的方法,如果能匹配
NSSet
的方法,则返回一个可以接收所有NSSet
对象可以接收的消息的代理对象; - 如果消息接收者所在的类的
+accessInstanceVariablesDirectly
方法返回的是YES
,就依次寻找名字能匹配
_<key>
、_is<Key>
、<key>
或者is<Key>
的实例变量; - 最后,调用
-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