在 Objective-C 程式碼中,方括號包住的 [receiver message] 語法看起來就像一般的方法呼叫。
但它和 C++ 或 Java 的方法呼叫有本質上的不同。
先從結論開始。
Objective-C 的方法呼叫不是「函式呼叫」,而是「傳送訊息」。
要執行哪段程式碼,是在執行階段決定,而不是編譯時期。
了解這項差異後,方法調配、KVO(Key-Value Observing)以及當機記錄中的unrecognized selector,都能串成一條線。這也是 Effective Objective-C 2.0 第 11~12 項涵蓋的內容。
先看重點摘要(3 點)
[obj foo]編譯後會變成 objc_msgSend(obj, @selector(foo)) 函式呼叫。- objc_msgSend 會在執行階段搜尋類別的方法表,找出要執行的函式(IMP)。
- 找不到時,不會立即當機,而是啟動稱為訊息轉送的三階段補救流程。
所有訊息都會匯集到 objc_msgSend
C++ 的虛擬函式會在編譯時期決定 vtable 索引。相對地,Objective-C 幾乎所有訊息都會通過 objc_msgSend 這個閘道。更精確地說,[super foo]會編譯成 objc_msgSendSuper;在 x86-64 時代,也曾有用於結構回傳的 objc_msgSend_stret 等同族函式(arm64 則整合到 objc_msgSend),但運作原理完全相同。
NSString *result = [greeting uppercaseString];
這段程式碼經過編譯器後,會變成如下的 C 函式呼叫。
NSString *result = objc_msgSend(greeting, @selector(uppercaseString));
objc_msgSend 做的事情很單純。
- 沿著 receiver 的 isa 指標找到類別
- 先搜尋類別的方法快取(非常快)
- 快取沒有時搜尋方法清單,仍沒有就往父類別查找
- 跳至找到的函式指標(IMP)
找到的結果會儲存在快取中,因此第二次起傳送相同訊息時,速度接近函式呼叫。這就是「動態分派很慢」在實務上幾乎不成問題的原因。
找不到時怎麼辦?訊息轉送的 3 個階段
方法搜尋一路到最上層類別仍失敗時,執行階段會在當機前提供三次機會。
第 1 階段:動態方法解析(resolveInstanceMethod:)
它會詢問類別:「現在要不要補上對應這個 selector 的方法?」Core Data 的 @dynamic 屬性會在此階段即時建立並加入存取子。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
第 2 階段:替代接收者(forwardingTargetForSelector:)
它會問:「如果你無法處理,能不能告訴我一個可以代勞的物件?」回傳其他物件後,訊息就會轉交給它。需要在不使用繼承的情況下模擬「類似多重繼承」時,會用到這個階段。
第 3 階段:完整轉送(forwardInvocation:)
進入此階段前,執行階段會先透過 methodSignatureForSelector: 詢問方法簽章。若回傳 nil,轉送甚至不會開始,而是直接進入 doesNotRecognizeSelector: 並當機。若有簽章,整個訊息會根據它包裝成 NSInvocation 物件後傳入。此處可以修改引數、分送給多個物件,或乾脆靜默吞掉。它最有彈性,但成本也最高。
直到 3 個階段全部失敗,才會真正發生那個著名的當機。
-[MyViewController buttonTapped:]: unrecognized selector sent to instance 0x7f8a2c400c50
現在就能理解,為什麼當機記錄不是說「沒有函式」,而是「無法辨識 selector」。因為編譯時期沒有問題,結果是執行階段的搜尋與轉送全部失敗。
這種結構帶來的功能
iOS 開發中熟悉的功能,都建構在這個執行階段結構之上。
- KVO:加入觀察者後,執行階段會悄悄把物件的 isa 替換成子類別
- 方法調配:在執行階段替換 selector 與 IMP 的連結
- NSProxy:建立只靠轉送運作的代理物件(也會用來解決 NSTimer 的循環參考)
- @dynamic:不在編譯時期建立存取子,而是在第 1 階段轉送中解決
Swift 選擇靜態分派與 vtable 分派,而不是訊息分派,以速度和安全性換取這份彈性。因此在 Swift 中使用 KVO 時,必須加上@objc dynamic。這表示要把該屬性重新交回 Objective-C 執行階段的訊息世界。
總結
- Objective-C 方法呼叫的本質,是透過 objc_msgSend 傳送訊息
- 要執行的程式碼在執行階段決定;有快取後,不必太擔心速度
- 搜尋失敗時,會依序經過動態解析 → 替代接收者 → 完整轉送來補救
- unrecognized selector 當機表示所有這些階段都失敗了
即使你不會接觸 Objective-C 遺留程式碼,這個執行階段仍在 UIKit 底層運作。當你開始用不同方式讀懂當機記錄中的一句話,除錯的思路也會改變。

