何為Object C Runtime
我們知道,Object C是 C語言的擴展,加入瞭面向對象的概念。
同時,Object C也是一種動態語言,所謂“動態”,是指該語言所編寫程序的最終的結構(如類的定義,對象的創建,方法的實現等),是在程序運行時動態的決定的。而靜態語言,則在程序的編譯期,就已經固定瞭程序的結構(如C++)。
換句話說,動態語言將靜態語言在編譯期所幹的一部分事情,延後到程序運行期來做。這樣就為我們提供瞭一種機會——可以在程序運行期“動態”的修改類的定義或方法的實現(Object C的黑魔法)。
同時,這也要求Object C除瞭編譯器外,還需要一種能夠在運行時對該語言進行動態解析的機制,這套動態解析機制就是我們所說的——Object C Runtime。Runtime是由C和編譯語言實現的。
與Runtime交互
理解瞭何為Runtime,那麼在平時編程中,如何與Object-C的Runtime進行交互呢?
Apple官方給出瞭如下三個途徑:
Object-C源碼
當我們在編譯OC中的class與method時, Object-C編譯器會在編譯階段對我們的源碼進行“改寫”,來對應生成具有動態特性的struct和方法調用(由於OC是基於C語言的,這些改寫後的代碼,也是C代碼,“喪失”瞭面向對象的表征,但是OC卻基於C實現面向對象的功能)。
經過改寫後的OC源碼,每一個class會對應一個C語言中的struct,該struct記錄瞭class中的各種信息:包括類定義(方法聲明,屬性),Category, protocol聲明等等。
同時,對於每一個類實例的method調用,均會改為Runtime方法的調用——objc_msgSend系列函數。該函數是OC實現method調用動態性,面向對象多態的基礎。在本文稍後將會對其進行討論。
NSObject方法
NSObject作為OC中絕大多數類的基類,在其中定義瞭若幹能夠與Runtime交互的函數,這樣子類也同樣的繼承瞭這些函數並有默認的實現。
如當一個類對象需要知道我是誰,我能做什麼時,可以調用:
description, isKindOfClass,isMemberOfClass,respondsToSelector,conformsToProtocol,methodForSelector等方法向Runtime系統索要相關信息。(註意上面說到,OC class會被改寫為支持動態特性的struct,而這些信息,應該是Runtime在那個struct中獲取的)
Runtime函數
與Runtime交互最直接的方式,就是調用Runtime函數瞭。Object-C中的Runtime函數是以動態庫的形式提供的,它被包含在目錄
/usr/include/objc
多數Runtime函數,會在編譯期,有編譯期為我們自動進行代碼”改寫”(將OC代碼改寫為Runtime代碼,函數)。同時NSObjcet類中,也提供瞭若幹Runtime函數。
對於Runtime函數,我們平常用到的很少,在後面的章節中,我會挑選若幹進行總結。
Object C 的NSObject類
在OC中,基本所有的類(NSProxy除外)均有一個共同的父類——NSObject。該類中定義的一切屬性和方法,均有其子類繼承。
可以說NSObject類為OC中所有的類定義瞭基本的實現和模板。
為瞭支持OC的動態特性,在NSObject定義瞭Class變量。
同時,NSObject提供瞭若幹與Runtime交互的接口方法。
我們的Runtime之旅,也就先從NSObject開始吧。
我們先來看NSObject類的聲明:
@interface NSObject { Class isa OBJC_ISA_AVAILABILITY; }
Class isa 是NSObject唯一的成員變量,同時也被每一個子類所繼承。isa是一個Class類型(實際上是一個objc_class結構體指針類型),他記錄並保存瞭當前類的若幹信息,同時還可以通過isa向上回溯到其父類,爺爺類,祖爺爺類…
OC中也以Class類型來表示一個類類型。
Class類型的定義如下(usr/include/objc/objc.h(該文件裡還有OC對象類型定義,id類型定義等有興趣可以看一下)):
/// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;
usr/include/objc/runtime.h
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
上面的定義中,可見多數成員變量已經被標記為不可用瞭。仍然可用的,似乎隻有
Class isa OBJC_ISA_AVAILABILITY;
Apple可能會在更新中,更改objc_class的實現,因此我們再去深究其定義,也沒有多大意義。
在這裡,我們隻需要知道幾點:
每一個Object-C 類,均含有一個Class類型的isa成員記錄瞭該類的必要信息。對應到類的實例,其內存分配中第一個成員,便是isa實例。反過來說,如果一個定義中含有Class類型isa成員,那麼該定義在Object-C中被當做類(或類的實例)處理。(isa是定義為類類型的充分必要條件) 對於Class類型,雖然是不透明的,因為其記錄瞭當前類的必要信息。我們仍可以猜測到:
Class中可以找到父類的Class 通過Class可以找到當前類的method列表 為瞭提高method調用效率,通過Class可以找到method cache表 通過Class中可以找到當前類中定義的變量,協議及分類信息。
objc_msgSend, Object-C的消息調用機制
在Object-C中,一切的方法調用,都是基於動態消息調用的。讓我們來看一看其具體實現。
objc_msgSend
當Object-C在編譯時,會將所有的類實例方法調用(在OC中,類本身也是一種類(meta-class)的實例),改寫為objc_msgSend方法(如果是直接調用super方法,則會改寫為objc_msgSendSuper, 另外還有兩個stret版本,大同小異,不做細究)。
objc_msgSend定義如下:
id objc_msgSend(id self, SEL op, ...);
第一個參數為指向方法調用的類對象指針。
第二個參數為所調用的selector
後面的‘…’ 為 所調用方法的參數列表
這樣,OC中一個方法調用
[receiver message];
被改寫為:
objc_msgSend(receiver, selector);
如果message還要接受參數,則改寫為
objc_msgSend(receiver, selector, arg1, arg2, ...)
objc_msgSend實現瞭方法動態綁定的一切必要操作:
尋找selector的對應實現,並調用之。(這個尋找的過程是動態的,我們可以在上面做點小文章) 向selector的實現傳遞receiver和參數。 將selector的返回值作為其自身objc_msgSend的返回值。
這裡的重點在於第一步,objc_msgSend方法是如何動態的找到selector的具體實現的。
之前我們知道,對於每一個Object-C 類,編譯器均會將其改寫為包含Class isa成員的結構體。而對於Object-C類實例,當其創建時,系同會為其分配內存及初始化各成員變量,而其成員變量的第一個變量,就是isa變量。isa是一個指針,指向當前類實例說對應的objc_class結構體,由於其對應於每一個class不同的定義,我們在這裡稱之為class struct。selector的動態實現,正是基於isa所指向的class struct的。
根據Apple的文檔,class struct中包含兩條重要信息:
1. 指向superclass的指針
2. 當前類的dispatch table。dispatch table中存儲瞭當前類中定義的所有方法的地址及其對應的selector。(類似於 selector:函數指針 的table結構)
根據上面兩條信息,objc_msgSend就能夠進行方法的動態綁定。步驟如下:
objc_msgSend函數根據當前對象的isa,找到其對應的class struct。在的class struct中,搜索dispatch table,是否含有對應的method selector。 如果找到,就根據method selector對應的函數地址,調用函數並傳遞參數,最終返回結果。 如果在當前的class struct的dispatch table中沒有找到,則根據class struct中的super class指針,繼續在其super class的dispatch table中尋找是否有對應的method selector實現。
如此遞歸實現,直到root class NSObject。
這一過程,如下圖所示:
這就是Runtime對於方法的動態綁定過程。
同時,為瞭提高selectZ喎?/kf/ware/vc/” target=”_blank” class=”keylink”>vcrXEsunV0tCnwsqjrNXrttTDv9K7uPZjbGFzc6OsT2JqZWN0LUO2vMzhuanBy9K7uPZzZWxlY3RvcjptZXRob2QgYWRkcmVzc7XEY2FjaGUgdGFibGWhoyDV4rj2Y2FjaGUgdGFibGXKx7bUw7/Su7j2Y2xhc3PAtMu1tcSjrLWx0ru49m1ldGhvZLG7tffTw7rzo6xzZWxlY3RvcjptZXRob2S21KOsvs274bG7tObI67Wxx7ByZWNlaXZlciBjbGFzc7XEY2FjaGXW0KOsyOe5+3NlbGVjdG9ytcTKtc/W1NrG5Li4wODW0KOsc2VsZWN0b3I6bWV0aG9kyNTIu7vhsbu05rW9cmVjZWl2ZXIgY2xhc3O1xGNhY2hl1tChozxiciAvPg0K1eLR+aOsz8jU2mNhY2hl1tDL0cv3o6zU2bW9ZGlzcGF0Y2ggdGFibGXW0MvRy/ejrMzhuN/By8vRy/fQp8LKoaM8L3A+DQo8aDMgaWQ9″selfcmd”>self,_cmd
上面說瞭method的動態綁定過程。
我們註意到,當方法被編譯器轉換成objc_msgSend函數後,除瞭方法必須的參數,objc_msgSend還會接收兩個特殊的參數:receiver 與 selector。
objc_msgSend(receiver, selector, arg1, arg2, ...)
receiver 表示當前方法調用的類實例對象。
selector則表示當前方法所對應的selector。
這兩個參數是編譯器自動填充的,我們在調用方法時,不必在源代碼中顯示傳入,因此可以被看做是“隱式參數”。
如果想要在source code中獲取這兩個參數,則可以用self(當前類實例對象)和_cmd(當前調用方法的selector)來表示。在AFNetWorking源碼中有這麼一段
- (NSArray *)tasks { return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; } - (NSArray *)dataTasks { return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; } - (NSArray *)uploadTasks { return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; } - (NSArray *)downloadTasks { return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; }
- (NSArray *)tasksForKeyPath:(NSString *)keyPath { ... [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) { tasks = dataTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) { tasks = uploadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) { tasks = downloadTasks; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) { tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"]; } ... }]; ... }
將_cmd作keyPath, 由於對於不同的方法調用,對應的_cmd也不同,因此能夠將其作為key來獲取不同種類的tasks。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath
來點黑魔法,方法的動態解析與message轉發
在Objective-C中,當我們調用類的實例或類中未定義方法時,通常會引發
unrecognized selector sent to… 異常,程序掛掉。
但是在runtime拋出異常之前,根據下面的文章:
glowing Objective C Runtime
它還會給你三次挽救的機會,包括“動態”的將方法添加到當前類實例(或類)中,將方法轉發給其他實例(類)。
這三次機會依次為:(Runtime會依次嘗試執行下面的機會,如果某個機會已經解決當前的‘困境’,則不會繼續向下執行)
Dynamic Method resolution Fast forwarding Normal forwarding
Dynamic Method resolution
當Runtime發現當前函數未定義時,首先會通過動態方法解析的方式,來嘗試尋找函數定義。
具體則為調用下面兩個函數之一:
+ (BOOL)resolveInstanceMethod:(SEL)sel; \\ 類實例方法 + (BOOL)resolveClassMethod:(SEL)sel; \\ 類方法
這兩個函數允許我們將函數動態的添加到類中。
Objective-C中的方法,實際上是至少接受兩個參數self,_cmd的C函數。
要實現動態添加方法到類中,則需要我們在resolveInstanceMethodresol \ resolveClassMethod 中通過Runtime方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
將對應的C函數添加到class中,同時,返回YES。
示例:
void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... }
+ (BOOL) resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSel]; }
討論
這種動態方法解析的作用在哪裡呢?既然仍需要提供方法實現,那直接在類中寫就好瞭啊?
在Objective-C的property特性中有一個關鍵字
@dynamic @dynamic propertyName;
@dynamic關鍵字告訴編譯器,property的相關方法會在運行時動態的提供。(註意到NSManagedObject的所有屬性均為@dynamic修飾)。
現在唯一想到的好處就是,方法實現能夠根據當時的系統狀態,動態的改變。
那是否有還有其他應用場景呢?
Fast message forwarding
當Runtime發現當前對象沒有實現selector時,會先調用resolveMethod,來動態解析當前的selector。如果resolveMethod沒有將SEL implement 添加到class中(無論其返回YES或NO)。則會繼續調用函數
- (id)forwardingTargetForSelector:(SEL)aSelector
將aSelector轉發至該函數返回的對象。
Message forwarding
如果前面兩個方法都沒有解決selector 的解析,iOS系統在向runtime拋出異常前,還會再給我們最後一個機會,那就是調用
- (void)forwardInvocation:(NSInvocation *)anInvocation
該函數僅接收一個NSInvocation參數,他封裝包含瞭調用信息與參數。在NSObject類中默認實現,但僅僅會調用doesNotRecognizeSelector:,導致拋出異常。子類可以重寫該方法。
顧名思義,該函數主要用來將發給對象的函數forward給其他對象。但在系統真正調用forwardInvocation函數前,由於該方法接受一個參數anInvocation,因此系統必須知道要解析的selector的簽名來組裝NSInvocation參數。所以,會調用方法 methodSignatureForSelector來返回一個selector signature,並放入NSInvocation中作為參數傳遞給forwardInvocation函數。
因此,使用forwardInvocation來轉發消息,需要下面兩個函數配合使用:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector - (void) forwardInvocation:(NSInvocation *)anInvocation
由於其用NSInvocation封裝瞭方法調用,其使用也很簡單。流程如下:
代碼示例如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(doMyHeavyThing)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; }else { return [super methodSignatureForSelector:aSelector]; } } - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([someOtherObject respondsToSelector: [anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation]; }
對於message forward的過程,下面的圖很好的做出瞭詮釋:
forwarding與繼承,代理
forwarding的實現,實際上是“借用”瞭其他類的方法實現。或者說是“繼承”瞭其他類的方法實現。在這一點上,可以說是繼承的一種實現方式,同時又不會因為繼承而產生臃腫的對象。
同時,根據Apple的文檔說法,forwarding也可以用來實現代理模式。及用一個輕量級的類,在程序中起到基本的做用於占位符。僅當必要時,輕量級的類才會將方法轉發給重量級類,重量級類才會被加載到內存中,從而實現瞭懶加載。
更“真實”的forwarding
forwarding可以用來模擬類的繼承,但是你卻無法做的更逼真。
當調用respondsToSelector: 和 isKindOfClass:方法時,調用的是NSObject的默認實現,會返回真實的值,而非forwarding的實現。
因此,為瞭能夠在程序中做到更真實的模擬繼承,則需要我們重寫如下函數:
respondsToSelector: isKindOfClass: instancesRespondToSelector: conformsToProtocol: methodSignatureForSelector:
總結
在本章中,我們瞭解瞭什麼是runtime,並探討瞭Class數據類型及變量isa。同時,利用isa變量,Objective-C實現瞭消息傳遞。
當前對象沒有對應的消息實現時,系統則會依次調用
(1)resolveInstance(class)Method 動態方法解析
(2)forwardingTargetForSelector (消息轉發)
(3)forwardInvocation(消息轉發)
來讓用戶在運行時提供消息的實現,如果這三次機會均沒有返回恰當的結果,則會拋出unrecognized selector異常。
在下一篇Runtime解析中,我們會對NSObject類中關於Runtime的函數做相應的解析。