本文章將記錄Objective-C中消息傳遞和轉發機制、Method Swizzling的相關資料,如有錯誤歡迎指出~ Objective-C 本質上是一種基於 C 語言的領域特定語言。C 語言是一門靜態語言,其在編譯時決定調用哪個函數。而 Objective-C 則是一門動態語言,其在編譯時不能決 ...
本文章將記錄Objective-C中消息傳遞和轉發機制、Method Swizzling的相關資料,如有錯誤歡迎指出~
Objective-C 本質上是一種基於 C 語言的領域特定語言。C 語言是一門靜態語言,其在編譯時決定調用哪個函數。而 Objective-C 則是一門動態語言,其在編譯時不能決定最終執行時調用哪個函數(Objective-C 中函數調用稱為消息傳遞)。Objective-C 的這種動態綁定機制正是通過 runtime 這樣一個中間層實現的。
消息傳遞(方法調用)
在 Objective-C 中,消息直到運行時才綁定到方法實現上。編譯器會將消息表達式轉化為一個消息函數的調用。
OC中的消息表達式如下(方法調用)
id returnValue = [someObject messageName:parameter];
編譯器看到這條消息會轉換成一條標準的 C 語言函數調用
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
我們可以看到轉換中,使用到了objc_msgSend
函數,這個函數將消息接收者和方法名作為主要參數,如下所示:
objc_msgSend(receiver, selector) // 不帶參數 objc_msgSend(receiver, selector, arg1, arg2,...) // 帶參數
objc_msgSend
通過以下幾個步驟實現了動態綁定機制。
- 首先,獲取
selector
指向的方法實現。由於相同的方法可能在不同的類中有著不同的實現,因此根據receiver
所屬的類進行判斷。 - 其次,傳遞
receiver
對象、方法指定的參數來調用方法實現。 - 最後,返回方法實現的返回值。
消息傳遞的關鍵在於【iOS面試糧食】Runtime—實例對象、類對象、元類對象記錄過的 objc_class
結構體,其有兩個關鍵的欄位:
isa
:指向父類的指針methodLists
: 類的方法分發表(dispatch table
)
當創建一個新對象時,先為其分配記憶體,並初始化其成員變數。其中 isa
指針也會被初始化,讓對象可以訪問類及類的繼承鏈。
下圖所示為消息傳遞過程的示意圖。
- 當消息傳遞給一個對象時,首先從運行時系統緩存
objc_cache
中進行查找。如果找到,則執行。否則,繼續執行下麵步驟。 objc_msgSend
通過對象的isa
指針獲取到類的結構體,然後在方法分發表methodLists
中查找方法的selector
。如果未找到,將沿著類的isa
找到其父類,併在父類的分發表methodLists
中繼續查找。- 以此類推,一直沿著類的繼承鏈追溯至
NSObject
類。一旦找到selector
,傳入相應的參數來執行方法的具體實現,並將該方法加入緩存objc_cache
。如果最後仍然沒有找到selector
,則會進入消息轉發流程
作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的點擊加入群聊iOS交流群:789143298 ,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿裡面試題、面試經驗,討論技術, 大家一起交流學習成長!
消息轉發
當一個對象能接收一個消息時,會走正常的消息傳遞流程。當一個對象無法接收某一消息時,會發生什麼呢?
- 預設情況下,如果以
[object message]
的形式調用方法,如果object
無法響應message
消息時,編譯器會報錯。 - 如果是以
performSeletor:
的形式調用方法,則需要等到運行時才能確定object
是否能接收message
消息。如果不能,則程式崩潰。
對於後者,當不確定一個對象是否能接收某個消息時,可以調用 respondsToSelector:
來進行判斷。
if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)]; }
事實上,當一個對象無法接收某一消息時,就會啟動所謂“消息轉發(message forwarding)”機制。通過消息轉發機制,我們可以告訴對象如何處理未知的消息。
消息轉發機制大致可分為三個步驟:
- 動態方法解析(Dynamic Method Resolution)
- 備用接收者
- 完整消息轉發
下圖所示為消息轉發過程的示意圖。
動態方法解析
這是整個消息轉發流程的第一個階段,如果在收到無法響應的消息後,會調用所屬類的方法:
//實例對象 + (BOOL)resolveInstanceMethod:(SEL)selector //類對象 + (BOOL)resolveClassMethod:(SEL)selector
其中參數selector
為未處理的方法。
返回值@return
表示能否新增一個方法來處理,一般使用@dynamic屬性來實現:
/************** 使用 resolveInstanceMethod 實現 @dynamic 屬性 **************/ id autoDictionaryGetter(id self, SEL _cmd); void autoDictionarySetter(id self, SEL _cmd, id value); + (BOOL)resolveInstanceMethod:(SEL)selector { NSString *selectorString = NSStringFromSelector(selector); if (/* selector is from a @dynamic property */) { if ([selectorString hasPrefix:@"set"]) { // 添加 setter 方法 class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@"); } else { // 添加 getter 方法 class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:"); } return YES; } return [super resolveInstanceMethod:selector]; }
備援接受者
這是整個消息轉發機制的第二站,看名字就可以看出來,這是在尋找一個備用援救的接受者,到了這一階段,系統會調用這個方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
傳入參數aSelector
同樣為無法處理的方法。
返回值為當前找到的備援接受者,如果沒有則返回nil,進入下一階段。
完整的消息轉發機制
如果前兩個階段都沒有辦法處理消息,就會啟動完整的消息轉發機制。
首先會創建NSInvocation
對象,把尚未處理的那條消息的全部信息細節裝在裡邊,在觸發NSInvocation
對象時,系統派發系統(message-dispatch system)將會把消息指派給目標對象。這時會調用該方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
傳入的參數anInvocation
就包含了消息的所有內容。
如果此時還是沒辦法處理消息,就會沿著繼承的順序一步一步向父類調用相同的方法,直到最後的NSObject
類中,這時候如果還沒有辦法處理消息,就會調用doesNotRecognizeSelector:
拋出異常。
到此為止,消息轉發的整個流程就都結束了。
Method Swizzling
談到黑科技,就不得不提一下Objective-C 中的 Method Swizzling 技術,它可以允許我們動態地替換方法的實現,實現 Hook
功能,是一種比子類化更加靈活的“重寫”方法的方式。就是說在開發中,我們可能會遇到系統提供的 API 不能滿足實際需求,我們希望能夠修改它以達到期望的效果。
Method Swizzling 原理
Method Swizzling 的實現充分利用了動態綁定機制。
在 Objective-C 中調用方法,其實是向一個對象發送消息,而查找消息的唯一依據是方法名 selector
。每個類都有一個方法列表 objc_method_list
,存放著其所有的方法 objc_method
。
typedef struct objc_method *Method struct objc_method{ SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; // 方法實現 }
每個方法 objc_method
保存了方法名(SEL
)和方法實現(IMP
)的映射關係。Method Swizzling 其實就是重置了 SEL
和 IMP
的映射關係。如下圖所示: