這是我遇到的一道面試題。雖然當時回答得一般,但是我很喜歡這道題目,因為它可以考察出面試者對運行時機制理解的深度和廣度,所以這里想試著重新回答一下。
現在的回答可能還很淺薄,如果以后想到了什么能補充的地方也會更新這篇( ̄▽ ̄)
本文中關于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