轉(zhuǎn)自:http://tech.glowing.com/cn/objective-c-runtime/
Objective-C
Objective-C 擴(kuò)展了 C 語(yǔ)言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機(jī)制。而這個(gè)擴(kuò)展的核心是一個(gè)用 C 和 編譯語(yǔ)言 寫的 Runtime 庫(kù)。它是 Objective-C 面向?qū)ο蠛蛣?dòng)態(tài)機(jī)制的基石。
Objective-C 是一個(gè)動(dòng)態(tài)語(yǔ)言,這意味著它不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來(lái)動(dòng)態(tài)得創(chuàng)建類和對(duì)象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)。理解 Objective-C 的 Runtime 機(jī)制可以幫我們更好的了解這個(gè)語(yǔ)言,適當(dāng)?shù)臅r(shí)候還能對(duì)語(yǔ)言進(jìn)行擴(kuò)展,從系統(tǒng)層面解決項(xiàng)目中的一些設(shè)計(jì)或技術(shù)問題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。
消息傳遞(Messaging)
I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal[sic] of Smalltalk is all about... The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
Alan Kay 曾多次強(qiáng)調(diào) Smalltalk 的核心不是面向?qū)ο螅嫦驅(qū)ο笾皇?strong>the lesser ideas,消息傳遞才是 the big idea。
在很多語(yǔ)言,比如 C ,調(diào)用一個(gè)方法其實(shí)就是跳到內(nèi)存中的某一點(diǎn)并開始執(zhí)行一段代碼。沒有任何動(dòng)態(tài)的特性,因?yàn)檫@在編譯時(shí)就決定好了。而在 Objective-C 中,[object foo]
語(yǔ)法并不會(huì)立即執(zhí)行 foo 這個(gè)方法的代碼。它是在運(yùn)行時(shí)給 object 發(fā)送一條叫 foo 的消息。這個(gè)消息,也許會(huì)由 object 來(lái)處理,也許會(huì)被轉(zhuǎn)發(fā)給另一個(gè)對(duì)象,或者不予理睬假裝沒收到這個(gè)消息。多條不同的消息也可以對(duì)應(yīng)同一個(gè)方法實(shí)現(xiàn)。這些都是在程序運(yùn)行的時(shí)候決定的。
事實(shí)上,在編譯時(shí)你寫的 Objective-C 函數(shù)調(diào)用的語(yǔ)法都會(huì)被翻譯成一個(gè) C 的函數(shù)調(diào)用 - objc_msgSend()
。比如,下面兩行代碼就是等價(jià)的:
[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 中,類、對(duì)象和方法都是一個(gè) 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ì)是一個(gè)有 objc_method
元素的可變長(zhǎng)度的數(shù)組。一個(gè) objc_method
結(jié)構(gòu)體中有函數(shù)名,也就是SEL,有表示函數(shù)類型的字符串 (見 Type Encoding) ,以及函數(shù)的實(shí)現(xiàn)IMP。
從這些定義中可以看出發(fā)送一條消息也就 objc_msgSend
做了什么事。舉 objc_msgSend(obj, foo)
這個(gè)例子來(lái)說:
首先,通過 obj 的 isa 指針找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中沒到 foo,繼續(xù)往它的 superclass 中找 ;
一旦找到 foo 這個(gè)函數(shù),就去執(zhí)行它的實(shí)現(xiàn)IMP .
但這種實(shí)現(xiàn)有個(gè)問題,效率低。但一個(gè) class 往往只有 20% 的函數(shù)會(huì)被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的 80% 。每個(gè)消息都需要遍歷一次 objc_method_list
并不合理。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來(lái),那可以大大提高函數(shù)查詢的效率。這也就是 objc_class
中另一個(gè)重要成員 objc_cache
做的事情 - 再找到 foo 之后,把 foo 的 method_name
作為 key ,method_imp
作為 value 給存起來(lái)。當(dāng)再次收到 foo 消息的時(shí)候,可以直接在 cache 里找到,避免去遍歷 objc_method_list
.
動(dòng)態(tài)方法解析和轉(zhuǎn)發(fā)
在上面的例子中,如果 foo
沒有找到會(huì)發(fā)生什么?通常情況下,程序會(huì)在運(yùn)行時(shí)掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前,Objective-C 的運(yùn)行時(shí)會(huì)給你三次拯救程序的機(jī)會(huì):
Method resolution
Fast forwarding
Normal forwarding
Method Resolution
首先,Objective-C 運(yùn)行時(shí)會(huì)調(diào)用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)。如果你添加了函數(shù)并返回 YES, 那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程。還是以 foo
為例,你可以這么實(shí)現(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 有用到這個(gè)方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在運(yùn)行時(shí)動(dòng)態(tài)添加的。
如果 resolve 方法返回 NO ,運(yùn)行時(shí)就會(huì)移到下一步:消息轉(zhuǎn)發(fā)(Message Forwarding)。
PS:iOS 4.3 加入很多新的 runtime 方法,主要都是以 imp 為前綴的方法,比如 imp_implementationWithBlock()
用 block 快速創(chuàng)建一個(gè) imp 。 上面的例子可以重寫成:
IMP fooIMP = imp_implementationWithBlock(^(id _self) { NSLog(@"Doing foo");});class_addMethod([self class], aSEL, fooIMP, "v@:");
Fast forwarding
如果目標(biāo)對(duì)象實(shí)現(xiàn)了 -forwardingTargetForSelector:
,Runtime 這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對(duì)象的機(jī)會(huì)。
- (id)forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == @selector(foo:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector];}
只要這個(gè)方法返回的不是 nil 和 self,整個(gè)消息發(fā)送的過程就會(huì)被重啟,當(dāng)然發(fā)送的對(duì)象會(huì)變成你返回的那個(gè)對(duì)象。否則,就會(huì)繼續(xù) Normal Fowarding 。
這里叫 Fast ,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機(jī)制。因?yàn)檫@一步不會(huì)創(chuàng)建任何新的對(duì)象,但下一步轉(zhuǎn)發(fā)會(huì)創(chuàng)建一個(gè) NSInvocation 對(duì)象,所以相對(duì)更快點(diǎn)。
Normal forwarding
這一步是 Runtime 最后一次給你挽救的機(jī)會(huì)。首先它會(huì)發(fā)送 -methodSignatureForSelector:
消息獲得函數(shù)的參數(shù)和返回值類型。如果 -methodSignatureForSelector:
返回 nil ,Runtime 則會(huì)發(fā)出 -doesNotRecognizeSelector:
消息,程序這時(shí)也就掛掉了。如果返回了一個(gè)函數(shù)簽名,Runtime 就會(huì)創(chuàng)建一個(gè) NSInvocation 對(duì)象并發(fā)送 -forwardInvocation:
消息給目標(biāo)對(duì)象。
NSInvocation 實(shí)際上就是對(duì)一個(gè)消息的描述,包括selector 以及參數(shù)等信息。所以你可以在 -forwardInvocation:
里修改傳進(jìn)來(lái)的 NSInvocation 對(duì)象,然后發(fā)送 -invokeWithTarget:
消息給它,傳進(jìn)去一個(gè)新的目標(biāo):
- (void)forwardInvocation:(NSInvocation *)invocation{ SEL sel = invocation.selector; if([alternateObject respondsToSelector:sel]) { [invocation invokeWithTarget:alternateObject]; } else { [self doesNotRecognizeSelector:sel]; }}
Cocoa 里很多地方都利用到了消息傳遞機(jī)制來(lái)對(duì)語(yǔ)言進(jìn)行擴(kuò)展,如 Proxies、NSUndoManager 跟 Responder Chain。NSProxy 就是專門用來(lái)作為代理轉(zhuǎn)發(fā)消息的;NSUndoManager 截取一個(gè)消息之后再發(fā)送;而 Responder Chain 保證一個(gè)消息轉(zhuǎn)發(fā)給合適的響應(yīng)者。
總結(jié)
Objective-C 中給一個(gè)對(duì)象發(fā)送消息會(huì)經(jīng)過以下幾個(gè)步驟:
在對(duì)象類的 dispatch table 中嘗試找到該消息。如果找到了,跳到相應(yīng)的函數(shù)IMP去執(zhí)行實(shí)現(xiàn)代碼;
如果沒有找到,Runtime 會(huì)發(fā)送 +resolveInstanceMethod:
或者 +resolveClassMethod:
嘗試去 resolve 這個(gè)消息;
如果 resolve 方法返回 NO,Runtime 就發(fā)送 -forwardingTargetForSelector:
允許你把這個(gè)消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象;
如果沒有新的目標(biāo)對(duì)象返回, Runtime 就會(huì)發(fā)送 -methodSignatureForSelector:
和 -forwardInvocation:
消息。你可以發(fā)送 -invokeWithTarget:
消息來(lái)手動(dòng)轉(zhuǎn)發(fā)消息或者發(fā)送 -doesNotRecognizeSelector:
拋出異常。
利用 Objective-C 的 runtime 特性,我們可以自己來(lái)對(duì)語(yǔ)言進(jìn)行擴(kuò)展,解決項(xiàng)目開發(fā)中的一些設(shè)計(jì)和技術(shù)問題。下一篇文章,我會(huì)介紹 Method Swizzling 技術(shù)以及如何利用 Method Swizzling 做 Logging。
Reference
Message forwarding
Objective-c-messaging
The faster objc_msgSend
Understanding objective-c runtime