文章目錄
- 前言
- 一、消息傳遞:objc_msgSend 的“查字典+遞歸找家長”流程
- 1. 第一步:查“最近調用記錄”(方法緩存)—— 最快即快速查找!
- 2. 第二步:翻“自己的字典”(類方法列表查找)—— 較慢!
- 3. 第三步:遞歸“找家長”(父類方法列表)—— 最慢!
- 總結:消息傳遞的“三級跳”
- 二、消息轉發:快遞送不到時的“三級補救方案”
- 階段 1:動態方法解析(自己加方法)——“我馬上補一個!”
- 一、消息傳遞的本質:`objc_msgSend`的執行流程
- 1. 快速查找:方法緩存(Method Cache)
- 2. 類方法列表查找
- 3. 緩存更新與結果返回
- 二、消息轉發:當消息無法被處理時
- 階段 1:動態方法解析(Dynamic Method Resolution)
- 階段 2:快速轉發(轉交給其他對象)——“我找朋友幫忙!”
- 階段 3:完整轉發(自定義處理流程)——“我自己寫個轉單系統!”
- 關鍵注意點:
- 三、底層原理:用匯編看 `objc_msgSend` 的“高效魔法”
- 四、總結
前言
??Objective-C的消息傳遞與消息轉發是其動態特性的核心,基于運行時(Runtime)系統實現。本文將從底層機制出發,詳細解析消息傳遞的完整流程及消息轉發的三個關鍵階段,并結合源碼(如 objc/runtime
)和匯編層面進行深入探討。
一、消息傳遞:objc_msgSend 的“查字典+遞歸找家長”流程
當調用 OC 對象的方法時(如 [obj doSomething]
),編譯器會將其轉換為 C 函數調用:
objc_msgSend(obj, @selector(doSomething));
objc_msgSend
是 OC 消息傳遞的核心函數,其本質是在接收者的類及其父類的方法列表中查找目標方法(SEL)的實現(IMP),并執行該實現。我們下面以OC 中調用方法(如 [dog 叫]
)為例子,本質是讓系統幫我們找到方法的實現代碼(IMP)并執行。這個過程由 objc_msgSend
函數完成,它的執行邏輯像“查字典+遞歸找家長”,分三步:
1. 第一步:查“最近調用記錄”(方法緩存)—— 最快即快速查找!
OC 運行時會為每個類維護一個方法緩存(methodCache_t
),用于加速方法查找。緩存的結構是一個哈希表,鍵為 SEL
(方法選擇子),值為 IMP
(方法實現的指針)。
objc_msgSend
首先檢查接收者類的緩存:
- 若緩存中存在目標
SEL
,直接跳轉到對應的IMP
執行(零成本緩存命中)。 - 若緩存未命中,進入類方法列表查找
例如,每個類(如 Dog
類)都有一個 方法緩存(Method Cache),類似手機的“最近通話記錄”:
- 作用:存“最近調用過的方法名(SEL)”和對應的“實現代碼地址(IMP)”,下次調用直接查緩存,無需重復計算。
- 為什么快:哈希表結構,查找時間復雜度接近 O(1)(常數級)。
例子:
你上周讓 dog
叫過 3 次,系統就把“叫”這個方法名(SEL)和對應的“汪汪汪”實現(IMP)記在 Dog
類的緩存里。這周再調用 [dog 叫]
,objc_msgSend
直接查緩存,秒級找到 IMP 并執行。
2. 第二步:翻“自己的字典”(類方法列表查找)—— 較慢!
若緩存未命中,objc_msgSend
會從接收者的當前類開始,逐級向上遍歷繼承鏈(直到 NSObject
或根類),在每個類的方法列表(method_list_t
)中查找目標 SEL
。
每個類的方法列表存儲了該類自身定義的方法(不包括父類)。若當前類未找到,繼續查找其父類的方法列表,直到根類(如 NSObject
)的父類為 nil
,此時查找失敗。
例子:
如果緩存里沒找到(比如第一次調用 [dog 叫]
),objc_msgSend
會去當前類的“字典”(方法列表)里找。每個類的方法列表存著自己定義的所有方法(類似字典的“正文”)。 Dog
類的字典里有 叫
、跑
等方法的定義(SEL 是“叫”,IMP 是“汪汪汪”的代碼)。objc_msgSend
遍歷這個字典,找到“叫”對應的 IMP,執行。
3. 第三步:遞歸“找家長”(父類方法列表)—— 最慢!
如果當前類的字典里也沒有(比如 Dog
類沒寫 叫
方法),objc_msgSend
會去父類的字典里繼續找(類似“問爸爸有沒有這個詞的解釋”)。一直找到根類(如 NSObject
)的父類(nil
),若最終找到目標 SEL
的 IMP
,則將該 SEL
與 IMP
的映射寫入當前類的方法緩存(后續調用直接命中緩存),并跳轉到 IMP
執行方法邏輯;還沒找到,就觸發消息轉發。
總結:消息傳遞的“三級跳”
調用 [dog 叫] → objc_msgSend 開始:
1.查 Dog 類的緩存 → 找到?直接執行(最快)。
2.沒找到 → 查 Dog 類的方法列表 → 找到?執行(較快)。
3.沒找到 → 遞歸查父類(Animal → NSObject)的方法列表 → 找到?執行(較慢)。
4.全沒找到 → 觸發消息轉發(兜底邏輯)。
二、消息轉發:快遞送不到時的“三級補救方案”
若 objc_msgSend
遍歷完緩存、當前類、父類繼承鏈仍未找到目標 SEL
的 IMP
,OC 運行時會觸發**消息轉發(Message Forwarding)**機制,嘗試通過一系列回調讓開發者有機會“補救”未處理的消息。消息轉發分為三個階段,按順序執行且不可逆(前一階段成功則后續階段不再觸發)。
階段 1:動態方法解析(自己加方法)——“我馬上補一個!”
Objective-C(OC)的消息傳遞與消息轉發是其動態特性的核心,基于運行時(Runtime)系統實現。本文將從底層機制出發,詳細解析消息傳遞的完整流程及消息轉發的三個關鍵階段,并結合源碼(如 objc/runtime
)和匯編層面進行深入探討。
一、消息傳遞的本質:objc_msgSend
的執行流程
當調用 OC 對象的方法時(如 [obj doSomething]
),編譯器會將其轉換為 C 函數調用:
objc_msgSend(obj, @selector(doSomething));
objc_msgSend
是 OC 消息傳遞的核心函數,其本質是在接收者的類及其父類的方法列表中查找目標方法(SEL)的實現(IMP),并執行該實現。整個過程可分為以下步驟:
1. 快速查找:方法緩存(Method Cache)
OC 運行時會為每個類維護一個方法緩存(methodCache_t
),用于加速方法查找。緩存的結構是一個哈希表,鍵為 SEL
(方法選擇子),值為 IMP
(方法實現的指針)。
objc_msgSend
首先檢查接收者類的緩存:
- 若緩存中存在目標
SEL
,直接跳轉到對應的IMP
執行(零成本緩存命中)。 - 若緩存未命中,進入類方法列表查找。
2. 類方法列表查找
若緩存未命中,objc_msgSend
會從接收者的當前類開始,逐級向上遍歷繼承鏈(直到 NSObject
或根類),在每個類的方法列表(method_list_t
)中查找目標 SEL
。
每個類的方法列表存儲了該類自身定義的方法(不包括父類)。若當前類未找到,繼續查找其父類的方法列表,直到根類(如 NSObject
)的父類為 nil
,此時查找失敗。
3. 緩存更新與結果返回
若最終找到目標 SEL
的 IMP
,則將該 SEL
與 IMP
的映射寫入當前類的方法緩存(后續調用直接命中緩存),并跳轉到 IMP
執行方法邏輯。
二、消息轉發:當消息無法被處理時
若 objc_msgSend
遍歷完繼承鏈仍未找到目標 SEL
的 IMP
,OC 運行時會觸發**消息轉發(Message Forwarding)**機制,嘗試通過一系列回調讓開發者有機會“補救”未處理的消息。消息轉發分為三個階段,按順序執行且不可逆(前一階段成功則后續階段不再觸發)。
階段 1:動態方法解析(Dynamic Method Resolution)
運行時首先調用類的類方法 +resolveInstanceMethod:
(針對實例方法)或 +resolveClassMethod:
(針對類方法),允許開發者動態添加方法實現。系統先問當前類:“你能自己寫一個這個方法嗎?”(調用 +resolveInstanceMethod:
)。這時候我們可以用 class_addMethod
動態添加方法實現,相當于“臨時補字典條目”。
例子:
我們發現 Dog
類忘記實現 叫
方法,于是在 +resolveInstanceMethod:
里補上:
+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(叫)) { // 動態添加方法:SEL 是“叫”,IMP 是“汪汪汪”的代碼class_addMethod(self, sel, (IMP)叫的實現, "v@:"); return YES; // 返回YES,表示消息已經被處理,即告訴系統:“我自己解決了!”,objc_msgSend會重新嘗試發送消息(此時緩存已更新)。}return [super resolveInstanceMethod:sel];
}// 方法實現(IMP)
void 叫的實現(id self, SEL _cmd) {NSLog(@"汪汪汪!");
}
如果成功,系統會把新方法加入緩存,下次調用直接命中。
階段 2:快速轉發(轉交給其他對象)——“我找朋友幫忙!”
如果動態解析失敗(+resolveInstanceMethod:
返回 NO
),運行時會調用實例方法 -forwardingTargetForSelector:
,允許開發者指定一個備用接收者(Forwarding Target),將消息轉發給該對象處理。動態解析失敗比如你不想自己加方法,系統問:“你能找個朋友(其他對象)幫我處理嗎?”(調用 -forwardingTargetForSelector:
)。你返回一個能處理該消息的對象,相當于“把快遞轉交給鄰居”。
例子:
Dog
類發現自己不會“叫”,但它的朋友 Cat
類會,于是返回 Cat
的實例:
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [Cat new]; // 找 Cat 幫忙}return [super forwardingTargetForSelector:aSelector];
}
若返回非 nil
對象,消息會被發送給該對象(相當于“代理”模式);若返回 nil
,進入下一階段。上述代碼中系統會把 [dog 叫]
轉發給 Cat
對象,如果 Cat
會“叫”,消息就被正確處理。
階段 3:完整轉發(自定義處理流程)——“我自己寫個轉單系統!”
若前兩階段均失敗(一般是快速轉發未提供備用接收者),運行時會觸發完整的消息轉發流程,核心是構造一個 NSInvocation
對象封裝消息信息,并調用 -forwardInvocation:
方法讓開發者自定義處理。就例如你找不到能幫忙的對象,系統啟動“完整轉發”:把消息(誰發的、方法名、參數)打包成 NSInvocation
對象,調用 -forwardInvocation:
讓你自定義處理。你需要自己決定如何處理這個消息(比如轉給其他對象、修改參數、記錄日志)。
例子:
你重寫 -forwardInvocation:
,把消息轉給 Cat
,并記錄日志:
- (void)forwardInvocation:(NSInvocation *)invocation {// 1. 獲取原消息的信息(方法名、參數)SEL sel = invocation.selector;id target = [Cat new]; // 臨時目標// 2. 修改消息目標為 Cat[invocation setTarget:target]; // 改成轉給 Cat[invocation invoke]; // 重新發送消息// 3. (可選)獲取返回值并處理id result;[invocation getReturnValue:&result];NSLog(@"轉發成功,結果是:%@", result);
}// 必須實現:獲取目標方法的簽名(NSMethodSignature),用于描述方法的參數、返回值類型等信息。若未實現此方法,會直接拋出 unrecognized selector異常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 無參數,返回 void}return [super methodSignatureForSelector:aSelector];
}
若 -methodSignatureForSelector:
未實現或返回 nil
,運行時會直接拋出 NSInvalidArgumentException
(unrecognized selector sent to instance
)。
關鍵注意點:
- 如果
-methodSignatureForSelector:
沒實現或返回nil
,系統會直接拋出unrecognized selector sent to instance
崩潰(常見錯誤)。
三、底層原理:用匯編看 objc_msgSend
的“高效魔法”
objc_msgSend
是用 ARM64 匯編 寫的,核心邏輯用幾行偽代碼概括:
objc_msgSend:// 1. 檢查接收者是否為 nil(OC 允許向 nil 發消息)cbz x0, LReturnNil // 如果 receiver 是 nil,直接返回 0// 2. 查緩存:從 receiver 的 isa 指針找到類,然后在緩存里找 SELldr x1, [x0] // x1 = receiver->isa(類的地址)CacheLookup // 匯編指令:在類的緩存里查 SEL 對應的 IMP// 3. 緩存命中:直接跳轉到 IMP 執行br x2 // x2 是緩存的 IMP 地址,跳轉執行LReturnNil:mov x0, #0 // 返回 0(對應 nil 消息的處理)ret
為什么快:緩存查找是匯編級別的優化,幾乎無額外開銷;方法列表查找是遞歸遍歷,但僅在緩存未命中時觸發。
四、總結
OC 的消息傳遞與轉發機制,本質是 “運行時動態性” 的體現:
- 高效性:通過緩存和方法列表的層級查找,平衡了“首次調用”和“重復調用”的性能。
- 靈活性:消息轉發的三階段設計,允許開發者在運行時動態修復未處理的方法(如 KVO、動態代理)。
一句話總結:objc_msgSend
像一個“智能快遞員”,先查最近記錄(緩存),再翻自己家抽屜(方法列表),最后遞歸問家長(父類);找不到時,系統給你三次“補救機會”(動態解析→快速轉發→完整轉發),確保消息“不輕易丟失”。**