SUBSCRIBEhtml
MENUios
04 JANUARY 2015 on objc, runtime, messagingobjective-c
Objective-C 擴展了 C 語言,並加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。數組
Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。理解 Objective-C 的 Runtime 機制能夠幫咱們更好的瞭解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 消息傳遞 (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 曾屢次強調 Smalltalk 的核心不是面向對象,面向對象只是 the lesser ideas,消息傳遞纔是 the big idea。app
在不少語言,好比 C ,調用一個方法其實就是跳到內存中的某一點並開始執行一段代碼。沒有任何動態的特性,由於這在編譯時就決定好了。而在 Objective-C 中,[object foo]
語法並不會當即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另外一個對象,或者不予理睬僞裝沒收到這個消息。多條不一樣的消息也能夠對應同一個方法實現。這些都是在程序運行的時候決定的。less
事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用 - objc_msgSend()
。好比,下面兩行代碼就是等價的:ide
[array insertObject:foo atIndex:5]; objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
消息傳遞的關鍵藏於 objc_object
中的 isa 指針和 objc_class
中的 class dispatch table。函數
objc_object
, objc_class
以及 Ojbc_method
在 Objective-C 中,類、對象和方法都是一個 C 的結構體,從 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
本質是一個有 objc_method
元素的可變長度的數組。一個 objc_method
結構體中有函數名,也就是SEL,有表示函數類型的字符串 (見 Type Encoding) ,以及函數的實現IMP。
從這些定義中能夠看出發送一條消息也就 objc_msgSend
作了什麼事。舉 objc_msgSend(obj, foo)
這個例子來講:
但這種實現有個問題,效率低。但一個 class 每每只有 20% 的函數會被常常調用,可能佔總調用次數的 80% 。每一個消息都須要遍歷一次 objc_method_list
並不合理。若是把常常被調用的函數緩存下來,那能夠大大提升函數查詢的效率。這也就是 objc_class
中另外一個重要成員 objc_cache
作的事情 - 再找到 foo 以後,把 foo 的 method_name
做爲 key ,method_imp
做爲 value 給存起來。當再次收到 foo 消息的時候,能夠直接在 cache 裏找到,避免去遍歷 objc_method_list
.
在上面的例子中,若是 foo
沒有找到會發生什麼?一般狀況下,程序會在運行時掛掉並拋出 unrecognized selector sent to … 的異常。但在異常拋出前,Objective-C 的運行時會給你三次拯救程序的機會:
首先,Objective-C 運行時會調用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函數實現。若是你添加了函數並返回 YES, 那運行時系統就會從新啓動一次消息發送的過程。仍是以 foo
爲例,你能夠這麼實現:
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 就是在運行時動態添加的。
若是 resolve 方法返回 NO ,運行時就會移到下一步:消息轉發(Message Forwarding)。
PS:iOS 4.3 加入不少新的 runtime 方法,主要都是以 imp 爲前綴的方法,好比 imp_implementationWithBlock()
用 block 快速建立一個 imp 。
上面的例子能夠重寫成:
IMP fooIMP = imp_implementationWithBlock(^(id _self) { NSLog(@"Doing foo"); }); class_addMethod([self class], aSEL, fooIMP, "v@:");
若是目標對象實現了 -forwardingTargetForSelector:
,Runtime 這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(foo:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
只要這個方法返回的不是 nil 和 self,整個消息發送的過程就會被重啓,固然發送的對象會變成你返回的那個對象。不然,就會繼續 Normal Fowarding 。
這裏叫 Fast ,只是爲了區別下一步的轉發機制。由於這一步不會建立任何新的對象,但下一步轉發會建立一個 NSInvocation 對象,因此相對更快點。
這一步是 Runtime 最後一次給你挽救的機會。首先它會發送 -methodSignatureForSelector:
消息得到函數的參數和返回值類型。若是 -methodSignatureForSelector:
返回 nil ,Runtime 則會發出 -doesNotRecognizeSelector:
消息,程序這時也就掛掉了。若是返回了一個函數簽名,Runtime 就會建立一個 NSInvocation 對象併發送 -forwardInvocation:
消息給目標對象。
NSInvocation 實際上就是對一個消息的描述,包括selector 以及參數等信息。因此你能夠在 -forwardInvocation:
裏修改傳進來的 NSInvocation 對象,而後發送 -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 就是專門用來做爲代理轉發消息的;NSUndoManager 截取一個消息以後再發送;而 Responder Chain 保證一個消息轉發給合適的響應者。
Objective-C 中給一個對象發送消息會通過如下幾個步驟:
+resolveInstanceMethod:
或者 +resolveClassMethod:
嘗試去 resolve 這個消息; -forwardingTargetForSelector:
容許你把這個消息轉發給另外一個對象; -methodSignatureForSelector:
和 -forwardInvocation:
消息。你能夠發送 -invokeWithTarget:
消息來手動轉發消息或者發送 -doesNotRecognizeSelector:
拋出異常。