你肯定聽過 oc 的本質就是發(fā)消息這句話, 這篇文章就來介紹一下 objc_msgSend 是什么以及它是如何工作的。
OC 發(fā)消息機制
Objective-C 的底層實現(xiàn)是 C, C 語言使用的是靜態(tài)綁定, 也就是說, 當程序在編譯期就知道了程序在運行期所調用的函數(shù), 而 OC 則是動態(tài)綁定, 也就是當程序在運行期的時候才能知道具體調用什么方法。
我們常常會寫到[object messageName:parameter], 其中 object 叫做"接收者", messageName 叫做"選擇子", parameter 為參數(shù), 選擇子和參數(shù)結合起來成為"消息"。
編譯期看到此消息后, 會將其轉換為 objc_msgSend 來調用, 這個函數(shù)會根據(jù)接受者和選擇子的類型來尋找并調用合適的方法, 在每個對象所屬的類中, 有一個"方法列表(objc_method_list)", 里面存了該類所有的"方法(objc_method)", 每個"方法"里包含 "方法名稱(SEL)" 和 "函數(shù)指針(IMP)", "函數(shù)指針"指向該方法的地址, 在每個對象所屬的類里面還有一個"快速映射表(objc_cache)", 當 object 發(fā)消息并且該消息沒有在"快速映射表"里時候, 會先判斷本類是否有與選擇子名稱相同的方法, 如果有會將該方法緩存起來并執(zhí)行, 當后續(xù)還使用該類調用了同樣的方法時, 會先去快速映射表里尋找
什么是 objc_msgSend?
objc_msgSend 的原型為:
void objc_msgSend(id self, SEL cmd, ...)
這是個"參數(shù)個數(shù)可變的函數(shù)", 可以接收兩個或兩個以上的參數(shù)。第一個參數(shù)為接收者, 第二個參數(shù)為選擇子, 后續(xù)參數(shù)為消息中的一些參數(shù)。
編譯期會把剛剛上述的[object messageName:parameter]轉換為下面這樣的函數(shù):
objc_msgSend(object, @selector(messageName:), parameter);
然后 objc_msgSend 函數(shù)會根據(jù)接收者和選擇子的類型來調用適當?shù)姆椒ā?/p>
下面為上述objc_method_list和objc_cache的結構
具體代碼在蘋果官方開源網(wǎng)站上: objc-runtime.h
struct objc_method_list {
#if defined(Release3CompatibilityBuild)
struct objc_method_list *method_next;
#else
struct objc_method_list *obsolete;
#endif
int method_count;
#ifdef __alpha__
int space;
#endif
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
} method_list[1]; /* variable length structure */
};
typedef struct objc_method *Method;
struct objc_cache {
unsigned int mask; /* total = mask + 1 */
unsigned int occupied;
Method buckets[1];
};
objc_method_list 是用于儲存 objc_method 的數(shù)組列表
objc_cache 是用于緩存調用過的 method
Method 是 objc_method 結構體的指針
消息派發(fā)(消息傳遞)
上述說到 objc_msgSend 函數(shù)會根據(jù)接收者和選擇子的類型來調用適當?shù)姆椒? 操作流程如下:
- 該方法會先在接收者所屬的類
objc_method_list中搜索, 如果能找到選擇子與方法名稱相同的方法則執(zhí)行 - 如果找不到, 就會沿著繼承體系向上尋找, 如果找到則執(zhí)行
- 如果最終還是沒有找到與之對應的方法, 那么將會觸發(fā)"消息轉發(fā)"的機制
消息轉發(fā)
當接收者所屬的類與向上的繼承體系都沒有找到該方法的時候, 會出發(fā)消息轉發(fā), 我們一定遇到過這樣的問題
-[__NSCFConstantString setTitle:forState:]: unrecognized selector sent to instance 0x100fb0200
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[__NSCFConstantString setTitle:forState:]: unrecognized selector sent to instance 0x100fb0200'
這段異常信息是有 NSObject 的 doesNotRecognizeSelector 所拋出的, 就是 __NSCFConstantString 無法處理 setTitle:forState: 這個方法, 在經(jīng)過了一系列的消息轉發(fā)后由 NSInvocation把這條消息所有的信息封裝起來, 調用了 NSObject 類的方法 doesNotRecognizeSelector 拋出異常。
消息轉發(fā)分為三步:
-
動態(tài)方法解析
對象在收到無法處理的消息時, 會先調用所屬類的
+ (BOOL)resolveInstanceMethod:(SEL)sel方法, 這個方法會判斷這個類能否新增一個實例方法來處理這個選擇子, 如果是收到的是類方法, 那么會調用+ (BOOL)resolveClassMethod:(SEL)sel來判斷。
上述方法執(zhí)行的條件必須開發(fā)者已經(jīng)寫好了具體實現(xiàn)代碼, 只是讓系統(tǒng)在運行期動態(tài)新增方法 -
備援接收者
如果第一步無法處理這條消息, 那么在備援接收者這一步, 會調用
- (id)forwardingTargetForSelector:(SEL)aSelector來尋找接收者是否有備援接收者, 如果有則返回該對象, 然后由這個對象去處理這條消息, 沒有的話則返回 nil, 返回 nil 說明這條消息沒有備援接收者, 然后會啟用完整的消息轉發(fā)。 -
完整的消息轉發(fā)
如果轉發(fā)算法已經(jīng)到了這里, 那么就要啟用完整的消息轉發(fā)機制了。首先系統(tǒng)會創(chuàng)建
NSInvocation對象, 把那條消息有關的全部信息封裝到這個對象中, 然后"消息派發(fā)系統(tǒng)"會調用
- (void)forwardInvocation:(NSInvocation *)anInvocation
把消息指派給目標對象。
實現(xiàn)此方法時, 會先判斷本類是否可以處理此方法, 如果不可以, 則會向上調用父類的方法, 直到 NSObject。 如果最后調到了NSObject 類的方法時, 仍未處理這條消息, 那么會調用doesNotRecognizeSelector方法來拋出異常。
總結
[object messageName:parameter]會先在編譯期轉為objc_msgSend(object, @selector(messageName:), parameter)- 在運行期根據(jù)對象所在類下的
objc_method_list或objc_cache尋找可執(zhí)行的方法并執(zhí)行- 如果找不到會沿繼承體系向上查找可執(zhí)行方法來執(zhí)行
- 如果仍然找不到會觸發(fā)消息轉發(fā)機制, 判斷這個類能否新增一個實例方法來處理這個選擇子
- 無法處理則尋找接收者是否有備援接收者, 并由備援接收者處理消息
- 創(chuàng)建并封裝
NSInvocation對象, "消息派發(fā)系統(tǒng)"把消息指派給目標對象, 一直向上調用父類方法, 到 NSObject 后仍未處理, 則拋出異常