Objective-C 擴展了 C 語言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機制烁巫。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫署隘。它是 Objective-C 面向?qū)ο蠛蛣討B(tài)機制的基石。
Objective-C 是一個動態(tài)語言程拭,這意味著它不僅需要一個編譯器定踱,也需要一個運行時系統(tǒng)來動態(tài)得創(chuàng)建類和對象、進行消息傳遞和轉(zhuǎn)發(fā)恃鞋。理解 Objective-C 的 Runtime 機制可以幫我們更好的了解這個語言崖媚,適當?shù)臅r候還能對語言進行擴展,從系統(tǒng)層面解決項目中的一些設(shè)計或技術(shù)問題恤浪。了解 Runtime 畅哑,要先了解它的核心 - 消息傳遞 (Messaging)。
消息傳遞(Messaging)
在很多語言水由,比如 C 荠呐,調(diào)用一個方法其實就是跳到內(nèi)存中的某一點并開始執(zhí)行一段代碼。沒有任何動態(tài)的特性砂客,因為這在編譯時就決定好了泥张。而在 Objective-C 中,[object foo] 語法并不會立即執(zhí)行 foo 這個方法的代碼鞠值。它是在運行時給 object 發(fā)送一條叫 foo 的消息媚创。這個消息,也許會由 object 來處理彤恶,也許會被轉(zhuǎn)發(fā)給另一個對象钞钙,或者不予理睬假裝沒收到這個消息鳄橘。多條不同的消息也可以對應(yīng)同一個方法實現(xiàn)。這些都是在程序運行的時候決定的芒炼。
事實上瘫怜,在編譯時你寫的 Objective-C 函數(shù)調(diào)用的語法都會被翻譯成一個 C 的函數(shù)調(diào)用 - objc_msgSend() 。比如本刽,下面兩行代碼就是等價的:
[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
消息傳遞的關(guān)鍵藏于 objc_object 中的 isa 指針和 objc_class 中的 class dispatch table鲸湃。
objc_object, objc_class 以及 Ojbc_method
在 Objective-C 中,類子寓、對象和方法都是一個 C 的結(jié)構(gòu)體唤锉,從 objc/objc.h 頭文件中,我們可以找到他們的定義:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
**struct objc_method_list **methodLists**;
**struct objc_cache *cache**;
struct objc_protocol_list *protocols;
#endif
};
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
objc_method_list 本質(zhì)是一個有 objc_method 元素的可變長度的數(shù)組别瞭。一個 objc_method 結(jié)構(gòu)體中有函數(shù)名窿祥,也就是SEL,有表示函數(shù)類型的字符串 (見 Type Encoding) 蝙寨,以及函數(shù)的實現(xiàn)IMP晒衩。
從這些定義中可以看出發(fā)送一條消息也就 objc_msgSend 做了什么事。舉 objc_msgSend(obj, foo) 這個例子來說:
首先墙歪,通過 obj 的 isa 指針找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中沒到 foo听系,繼續(xù)往它的 superclass 中找 ;
一旦找到 foo 這個函數(shù),就去執(zhí)行它的實現(xiàn)IMP .
但這種實現(xiàn)有個問題虹菲,效率低靠胜。但一個 class 往往只有 20% 的函數(shù)會被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的 80% 毕源。每個消息都需要遍歷一次 objc_method_list 并不合理浪漠。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來,那可以大大提高函數(shù)查詢的效率霎褐。這也就是 objc_class 中另一個重要成員 objc_cache 做的事情 - 再找到 foo 之后址愿,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來冻璃。當再次收到 foo 消息的時候响谓,可以直接在 cache 里找到,避免去遍歷 objc_method_list.
動態(tài)方法解析和轉(zhuǎn)發(fā)
在上面的例子中省艳,如果 foo 沒有找到會發(fā)生什么娘纷?通常情況下,程序會在運行時掛掉并拋出 unrecognized selector sent to … 的異常跋炕。但在異常拋出前赖晶,Objective-C 的運行時會給你三次拯救程序的機會:
Method resolution
Fast forwarding
Normal forwarding
Method Resolution
首先,Objective-C 運行時會調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:枣购,讓你有機會提供一個函數(shù)實現(xiàn)嬉探。如果你添加了函數(shù)并返回 YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程棉圈。還是以 foo 為例涩堤,你可以這么實現(xiàn):
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}
Core Data 有用到這個方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在運行時動態(tài)添加的分瘾。
如果 resolve 方法返回 NO 胎围,運行時就會移到下一步:消息轉(zhuǎn)發(fā)(Message Forwarding)。
PS:iOS 4.3 加入很多新的 runtime 方法德召,主要都是以 imp 為前綴的方法白魂,比如 imp_implementationWithBlock() 用 block 快速創(chuàng)建一個 imp 。
上面的例子可以重寫成:
IMP fooIMP = imp_implementationWithBlock(^(id _self) {
NSLog(@"Doing foo");
});
class_addMethod([self class], aSEL, fooIMP, "v@:");
Fast forwarding
如果目標對象實現(xiàn)了 -forwardingTargetForSelector: 上岗,Runtime 這時就會調(diào)用這個方法福荸,給你把這個消息轉(zhuǎn)發(fā)給其他對象的機會。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(foo:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
只要這個方法返回的不是 nil 和 self肴掷,整個消息發(fā)送的過程就會被重啟敬锐,當然發(fā)送的對象會變成你返回的那個對象。否則呆瞻,就會繼續(xù) Normal Fowarding 台夺。
這里叫 Fast ,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機制痴脾。因為這一步不會創(chuàng)建任何新的對象颤介,但下一步轉(zhuǎn)發(fā)會創(chuàng)建一個 NSInvocation 對象,所以相對更快點赞赖。
Normal forwarding
這一步是 Runtime 最后一次給你挽救的機會滚朵。首先它會發(fā)送 -methodSignatureForSelector: 消息獲得函數(shù)的參數(shù)和返回值類型。如果 -methodSignatureForSelector: 返回 nil 前域,Runtime 則會發(fā)出 -doesNotRecognizeSelector: 消息始绍,程序這時也就掛掉了。如果返回了一個函數(shù)簽名话侄,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送 -forwardInvocation: 消息給目標對象亏推。
NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數(shù)等信息年堆。所以你可以在 -forwardInvocation: 里修改傳進來的 NSInvocation 對象吞杭,然后發(fā)送 -invokeWithTarget: 消息給它,傳進去一個新的目標:
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = invocation.selector;
if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
Cocoa 里很多地方都利用到了消息傳遞機制來對語言進行擴展变丧,如 Proxies芽狗、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來作為代理轉(zhuǎn)發(fā)消息的痒蓬;NSUndoManager 截取一個消息之后再發(fā)送童擎;而 Responder Chain 保證一個消息轉(zhuǎn)發(fā)給合適的響應(yīng)者滴劲。
總結(jié)
Objective-C 中給一個對象發(fā)送消息會經(jīng)過以下幾個步驟:
在對象類的 dispatch table 中嘗試找到該消息。如果找到了顾复,跳到相應(yīng)的函數(shù)IMP去執(zhí)行實現(xiàn)代碼班挖;
如果沒有找到,Runtime 會發(fā)送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個消息芯砸;
如果 resolve 方法返回 NO萧芙,Runtime 就發(fā)送 -forwardingTargetForSelector: 允許你把這個消息轉(zhuǎn)發(fā)給另一個對象;
如果沒有新的目標對象返回假丧, Runtime 就會發(fā)送 -methodSignatureForSelector:和 -forwardInvocation: 消息双揪。你可以發(fā)送 -invokeWithTarget: 消息來手動轉(zhuǎn)發(fā)消息或者發(fā)送 -doesNotRecognizeSelector: 拋出異常。
利用 Objective-C 的 runtime 特性包帚,我們可以自己來對語言進行擴展渔期,解決項目開發(fā)中的一些設(shè)計和技術(shù)問題。下一篇文章渴邦,我會介紹 Method Swizzling 技術(shù)以及如何利用 Method Swizzling 做 Logging擎场。