Objective-C是一門動態語言,可以在運行的時候動態決定調用哪個方法實現,甚至增加、替換方法的具體實現,而這些都歸功於Objective-C的運行時(runtime)系統。本篇文章,我們就從消息發送的角度來看下Objective-C的運行時。
0. 決定方法調用的動態性
Objective-C語言是一門面向對象編程語言,而面向對象的一個基本特征就是多態。在一個復雜的類的繼承層次結構中,子類可以和父類具有同名的方法(override),父類的引用也可以接受子類對象。而在這種情況下,調用父類引用的方法(或者說發送某個消息),那麼如果這個方法在父類和子類中的實現邏輯不同,哪種實現會被執行呢,答案自然應該是子類的實現邏輯被調用執行。這是面向對象語言的基本特性之一。
C作為一門非面向對象語言,肯定是不支持這些的,而建立在C基礎之上的Objective-C通過運行時庫做到了這點。比如有兩個類,ParentClass和它的子類SonClass,具有同名不同實現的無參實例方法(“-”)doSomething,instance是一個ParentClass*類型引用,但卻被指向了一個SonClass的實際對象。那麼調用[instance doSomething]; 毫無疑問會執行SonClass裡的邏輯。
這怎麼做到?這正是通過Objective-C運行時對消息發送的分派機制。贅言無益,看看下面一段代碼就明白了:
void funcA() { printf("hello world!\n"); } void funcB() { printf("world hello!\n"); } void receiveMessage() { void (*function)(); if ( shouldFunctionAorB ) { function = &funcA; } else { function = &funcB; } (*function)(); }
這是一段C語言代碼,使用了函數指針,函數主體邏輯在receiveMessage函數裡,它通過運行時shouldFunctionAorB變量的狀態選擇最終執行funcA還是funcB。
雖然本人未看過Objective-C運行時庫的源代碼,但相信其實現的方式與此並無太大區別,原理就是如此。而決定運行時執行哪個方法實現的條件可能會比較復雜,但runtime肯定是可以得到並以此決斷的。
1. SEL和IMP
使用過UIControl的朋友應該知道addTarget:action:forControlEvents:這個方法,裡面的action參數通常用到一個@selector(),而這個語句的結果就是得到了一個SEL變量。SEL變量我們就叫做一個selector。Selector其實很好理解,就是在發送消息/方法調用時標識是哪個消息(方法)的東西。這個通常都是通過方法全名得來的,就比如上面UIControl的(addTarget:action:forControlEvents:)。
IMP實際上就是具體的一個方法邏輯實現(implementation,貌似IMP就是這麼來的)。這個和上面代碼示例中的C語言函數指針概念相似。到funcA的指針和到funcB的指針都是IMP變量。
Obejctive-C的運行時系統提供的消息的分派機制把SEL和IMP關聯起來。但每次消息發送/方法調用都做一遍查找顯然是很麻煩很低效的,所以在Objective-C的runtime這裡一定是有緩存機制的, 使得對每個類特定selector對應的IMP可以很快找到。
當然,即使用最好的數據結構和最快的查找算法,和直接執行C函數調用的靜態綁定方式相比,也一定有性能損失。但比起Objective-C運行時為開發者提供的諸多動態特性相比,這些都是值得的。
2. objc_msgSend
0中的代碼例子中闡述了Objective-C中消息發送的原理,而1中也解釋了SEL和IMP的概念。那麼當這一切都清楚了的時候,實際消息發送的時候是怎麼操作的呢?那就是通過objc_msgSend這一系列的運行時C函數調用來做到的了。
在蘋果官方文檔《Objective-C Runtime Programming Guide》中提出和Objective-C運行時交互有三種方式,其中最底層的一種方式就是直接使用runtime的函數。我們下面就來看下和發送消息直接相關的幾個函數:
objc_msgSend objc_msgSend_fpret objc_msgSend_stret objc_msgSendSuper objc_msgSendSuper_stret
我們看到他們都是以objc_msgSend開頭的,也就是Objective-C“消息發送”的意思,我們就來看最基礎的,也就是第一個。
id objc_msgSend ( id self, SEL op, ... );
我們看到後面有self、op和變長參數列表。我們知道對於不同的兩個Objective-C的類對象,執行消息發送結果可能是不同的,那麼第一個參數id類型的參數self就是用來標識不同對象的,而SEL的op就是標識發送哪個消息/調用哪個方法的selector,後面的變參我們其實可以猜想到,就是selector對應方法的實際參數。
而具體找IMP的過程顯然被包裝在objc_msgSend裡面,或者說selector和IMP的對應關系被運行時系統記錄了下來,無需objc_msgSend的調用者直接關注。
這樣看來,這個objc_msgSend實際上就相當於0中示例代碼的receiveMessage函數了。
上面列表中objc_msgSend一族的其它幾個函數其實功能上是差不多的:
1) 帶有ret結尾的標識返回值在某些情況下可以特殊處理(比如使用處理器的寄存器存儲而非開辟棧空間),以進行優化。
2) 帶有super的標識的回去找id參數的父類。關於怎麼找一個Objective-C的當前類和父類,本小站後面的技術文章會找機會解釋。
3. Method swizzling
上問提到了SEL和IMP的對應關系,那麼這個能不能改呢?了解運行時機制除了知道objc_msgSend原理和怎麼調用外還有什麼意義?Method swizzling就是兩個問題的一個很好的答案。
Method swizzling比較直接的翻譯就是方法(實現)交換(“攪合”)。下面是一個簡單的例子(源自《Effective Objective-C》):
Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString)); method_exchangeImplementations(originalMethod, swappedMethod);
這樣,NSString之後的lowercaseString和uppercaseString就是相反的效果了!
方法實現的交換可以有很多應用場景,尤其在調試系統庫等領域。
4. 消息轉發
我不知道閱讀本文的朋友們在開發調試時有沒有遇到過“unrecognized selector”這個異常。這個錯誤提示告訴我們在程序運行時調用的方法沒找到對應的實現,通常一個app就這樣直接crash了。而其實這個情況系統是做過處理嘗試的,最終這個異常也是系統庫中NSObject的方法實現拋出來的。
這一段我們整理下Objective-C運行時中的消息轉發機制,然後我們就明白上面這個異常提示是怎麼出來的了。
消息轉發通常是在Objective-C運行時系統找不到一個selector對應的實現時,這時候系統會回過來詢問這個對象所屬的類應該怎麼做,主要分為幾步:
1) 要不要對此次消息調用(實例方法/類方法)做動態解析和執行處理。這個我們可以通過class_addMethod函數,給這個類一個IMP,這樣就可以去執行了。很多@dynamic的標記就需要這麼配合來做。
2) 不做動態解析,那麼是否轉移消息給別的對象,轉給誰。
3) 不轉給別的對象,那麼這次消息發送通過調用哪個Invocation對象來處理。
如上過程可參看如下取自《Effective Objective-C》的截圖:
實際上,所有類的forwardInvocation方法都從NSObject那裡繼承到了,而其會調用:
- (void)doesNotRecognizeSelector:(SEL)aSelector;
除了這些,消息轉發很強大,用好了大有作為,比如可以做到類似C++中多繼承的一些效果。