消息發送和轉發流程可以概括為:消息發送(Messaging)是 Runtime 通過 selector 快速查找 IMP 的過程,有了函數指針就可以執行對應的方法實現;消息轉發(Message Forwarding)是在查找 IMP 失敗后執行一系列轉發流程的慢速通道,如果不作轉發處理,則會打日志和拋出異常。
本文不講述開發者在消息發送和轉發流程中需要做的事,而是講述原理。能夠很好地閱讀本文的前提是你對 Objective-C Runtime 已經有一定的了解,關于什么是消息,Class 的結構,selector、IMP、元類等概念將不再贅述。本文用到的源碼為 objc4-680 和 CF-1153.18,逆向 CoreFoundation.framework 的系統版本為 macOS 10.11.5,匯編語言架構為 x86_64。
八面玲瓏的 objc_msgSend
此函數是消息發送必經之路,但只要一提 objc_msgSend,都會說它的偽代碼如下或類似的邏輯,反正就是獲取 IMP 并調用:
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
- 源碼解析
為啥老用偽代碼?因為 objc_msgSend 是用匯編語言寫的,針對不同架構有不同的實現。如下為 x86_64 架構下的源碼,可以在 objc-msg-x86_64.s 文件中找到,關鍵代碼如下:
ENTRY _objc_msgSend
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL // calls IMP on success
NilTestSupport NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r11
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
END_ENTRY _objc_msgSend
這里面包含一些有意義的宏:
- NilTest 宏,判斷被發送消息的對象是否為 nil
的。如果為 nil,那就直接返回 nil。這就是為啥也可以對 nil 發消息。 - GetIsaFast 宏可以『快速地』獲取到對象的 isa 指針地址(放到 r11 寄存器,r10 會被重寫;在 arm 架構上是直接賦值到 r9)
- CacheLookup 這個宏是在類的緩存中查找 selector 對應的 IMP(放到 r10)并執行。如果緩存沒中,那就得到 Class 的方法表中查找了。
- MethodTableLookup
宏是重點,負責在緩存沒命中時在方法表中負責查找 IMP:
.macro MethodTableLookup
MESSENGER_END_SLOW
SaveRegisters
// _class_lookupMethodAndLoadCache3(receiver, selector, class)
movq $0, %a1
movq $1, %a2
movq %r11, %a3
call __class_lookupMethodAndLoadCache3
// IMP is now in %rax
movq %rax, %r11
RestoreRegisters
.endmacro
從上面的代碼可以看出方法查找 IMP 的工作交給了 OC 中的 _class_lookupMethodAndLoadCache3 函數,并將 IMP 返回(從 r11 挪到 rax)。最后在 objc_msgSend 中調用 IMP。
為什么使用匯編語言
其實在 objc-msg-x86_64.s 中包含了多個版本的 objc_msgSend 方法,它們是根據返回值的類型和調用者的類型分別處理的:objc_msgSendSuper:向父類發消息,返回值類型為 id
objc_msgSend_fpret:返回值類型為 floating-point,其中包含 objc_msgSend_fp2ret 入口處理返回值類型為 long double 的情況
objc_msgSend_stret:返回值為結構體
objc_msgSendSuper_stret:向父類發消息,返回值類型為結構體
當需要發送消息時,編譯器會生成中間代碼,根據情況分別調用 objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret 其中之一。
這也是為什么 objc_msgSend 要用匯編語言而不是 OC、C 或 C++ 語言來實現,因為單獨一個方法定義滿足不了多種類型返回值,有的方法返回 id,有的返回 int??紤]到不同類型參數返回值排列組合映射不同方法簽名(method signature)的問題,那 switch 語句得老長了。。。**這些原因可以總結為 Calling Convention,
也就是說函數調用者與被調用者必須約定好參數與返回值在不同架構處理器上的存取規則,比如參數是以何種順序存儲在棧上,或是存儲在哪些寄存器上。**除此之外還有其他原因,比如其可變參數用匯編處理起來最方便,因為找到 IMP 地址后參數都在棧上。要是用 C++ 傳遞可變參數那就悲劇了,prologue 機制會弄亂地址(比如 i386 上為了存儲 ebp向后移位 4byte),最后還要用 epilogue 打掃戰場。而且匯編程序執行效率高,在 Objective-C Runtime 中調用頻率較高的函數好多都用匯編寫的。
使用 lookUpImpOrForward 快速查找 IMP
上一節中說到的 _class_lookupMethodAndLoadCache3
函數其實只是簡單的調用了 lookUpImpOrForward 函數:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意 lookUpImpOrForward 調用時使用緩存參數傳入為 NO,因為之前已經嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)實現了一套查找 IMP 的標準路徑,也就是在消息轉發(Forward)之前的邏輯。
優化緩存查找&類的初始化
先對 debug 模式下的 assert 進行 unlock:
runtimeLock.assertUnlocked();
runtimeLock
本質上是對 Darwin 提供的線程讀寫鎖 pthread_rwlock_t 的一層封裝,提供了一些便捷的方法。
lookUpImpOrForward 接著做了如下兩件事:
如果使用緩存(cache 參數為 YES),那就調用 cache_getImp 方法從緩存查找 IMP。cache_getImp是用匯編語言寫的,也可以在 objc-msg-x86_64.s 找到,其依然用了之前說過的 CacheLookup 宏。因為 _class_lookupMethodAndLoadCache3 調用 lookUpImpOrForward 時 cache 參數為 NO,這步直接略過。
如果是第一次用到這個類且 initialize 參數為 YES(initialize && !cls->isInitialized()),需要進行初始化工作,也就是開辟一個用于讀寫數據的空間。先對 runtimeLock 寫操作加鎖,然后調用 cls
的 initialize 方法。如果 sel == initialize 也沒關系,雖然 initialize 還會被調用一次,但不會起作用啦,因為 cls->isInitialized() 已經是 YES 啦。
繼續在類的繼承體系中查找
考慮到運行時類中的方法可能會增加,需要先做讀操作加鎖,使得方法查找和緩存填充成為原子操作。添加 category 會刷新緩存,之后如果舊數據又被重填到緩存中,category 添加操作就會被忽略掉。
runtimeLock.read();
之后的邏輯整理如下:
如果 selector 是需要被忽略的垃圾回收用到的方法,則將 IMP 結果設為 _objc_ignored_method,這是個匯編程序入口,可以理解為一個標記。對此種情況進行緩存填充操作后,跳到第 7 步;否則執行下一步。
查找當前類中的緩存,跟之前一樣,使用 cache_getImp 匯編程序入口。如果命中緩存獲取到了 IMP,則直接跳到第 7 步;否則執行下一步。
在當前類中的方法列表(method list)中進行查找,也就是根據 selector 查找到 Method 后,獲取 Method 中的 IMP(也就是 method_imp 屬性),并填充到緩存中。查找過程比較復雜,會針對已經排序的列表使用二分法查找,未排序的列表則是線性遍歷。如果成功查找到 Method 對象,就直接跳到第 7 步;否則執行下一步。
在繼承層級中遞歸向父類中查找,情況跟上一步類似,也是先查找緩存,緩存沒中就查找方法列表。這里跟上一步不同的地方在于緩存策略,有個 _objc_msgForward_impcache 匯編程序入口作為緩存中消息轉發的標記。也就是說如果在緩存中找到了 IMP,但如果發現其內容是 _objc_msgForward_impcache,那就終止在類的繼承層級中遞歸查找,進入下一步;否則跳到第 7 步。
當傳入 lookUpImpOrForward 的參數 resolver 為 YES 并且是第一次進入第 5 步時,時進入動態方法解析;否則進入下一步。這步消息轉發前的最后一次機會。此時釋放讀入鎖(runtimeLock.unlockRead()
),接著間接地發送 +resolveInstanceMethod 或 +resolveClassMethod 消息。這相當于告訴程序員『趕緊用 Runtime 給類里這個 selector 弄個對應的 IMP 吧』,因為此時鎖已經 unlock 了所以不會緩存結果,甚至還需要軟性地處理緩存過期問題可能帶來的錯誤。這里的業務邏輯稍微復雜些,后面會總結。因為這些工作都是在非線程安全下進行的,完成后需要回到第 1 步再次查找 IMP。
此時不僅沒查找到 IMP,動態方法解析也不奏效,只能將 _objc_msgForward_impcache 當做 IMP 并寫入緩存。這也就是之前第 4 步中為何查找到 _objc_msgForward_impcache 就表明了要進入消息轉發了。
讀操作解鎖,并將之前找到的 IMP 返回。(無論是正經 IMP 還是不正經的 _objc_msgForward_impcache)這步還偏執地做了一些腦洞略大的 assert,很有趣。
對于第 5 步,其實是直接調用 _class_resolveMethod 函數,在這個函數中實現了復雜的方法解析邏輯。如果 cls 是元類則會發送 +resolveClassMethod,然后根據 lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/) 函數的結果來判斷是否發送 +resolveInstanceMethod;如果不是元類,則只需要發送 +resolveInstanceMethod 消息。這里用 +resolveInstanceMethod 或 +resolveClassMethod 時再次用到了 objc_msgSend,而且第三個參數正是傳入 lookUpImpOrForward 的那個 sel。在發送方法解析消息之后還會調用 lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/) 來判斷是否已經添加上 sel 對應的 IMP 了,打印出結果。
最后 lookUpImpOrForward 方法也會把真正的 IMP 或者需要消息轉發的 _objc_msgForward_impcache 返回,并最終專遞到 objc_msgSend 中。而 _objc_msgForward_impcache 會在轉化成 _objc_msgForward 或 _objc_msgForward_stret。這個后面會講解原理。
回顧 objc_msgSend 偽代碼
回過頭來會發現 objc_msgSend 的偽代碼描述得很傳神啊,因為class_getMethodImplementation 的實現如下:
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
lookUpImpOrNil
函數獲取不到 IMP 時就返回 _objc_msgForward,后面會講到它。lookUpImpOrNil 跟 lookUpImpOrForward 的功能很相似,只是將 lookUpImpOrForward 實現中的 _objc_msgForward_impcache
替換成了 nil:
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
lookUpImpOrNil 方法可以查找到 selector 對應的 IMP 或是 nil,所以如果不考慮返回值類型為結構體的情況,用那幾行偽代碼來表示復雜的匯編實現還是挺恰當的。
forwarding 中路漫漫的消息轉發
- objc_msgForward_impcache 的轉換
_objc_msgForward_impcache 只是個內部的函數指針,只存儲于上節提到的類的方法緩存中,需要被轉化為 _objc_msgForward 和 _objc_msgForward_stret 才能被外部調用。但在Mac OS XmacOS 10.6 及更早版本的 libobjc.A.dylib 中是不能直接調用的,況且我們根本不會直接用到它。帶 stret 后綴的函數依舊是返回值為結構體的版本。
上一節最后講到如果沒找到 IMP,就會將 _objc_msgForward_impcache 返回到 objc_msgSend 函數,而正是因為它是用匯編語言寫的,所以將內部使用的 _objc_msgForward_impcache 轉化成外部可調用的 _objc_msgForward 或 _objc_msgForward_stret 也是由匯編代碼來完成。實現原理很簡單,就是增加個靜態入口 __objc_msgForward_impcache,然后根據此時 CPU 的狀態寄存器的內容來決定轉換成哪個。如果是 NE
(Not Equal) 則轉換成 _objc_msgForward_stret,反之是 EQ
(Equal) 則轉換成 _objc_msgForward:
jne __objc_msgForward_stret
jmp __objc_msgForward
為何根據狀態寄存器的值來判斷轉換成哪個函數指針呢?回過頭來看看 objc_msgSend 中調用完 MethodTableLookup 之后干了什么:
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
再看看返回值為結構體的 objc_msgSend_stret 這里的邏輯:
MethodTableLookup %a2, %a3 // r11 = IMP
test %r11, %r11 // set ne (stret) for forward; r11!=0
jmp *%r11 // goto *imp
稍微懂匯編的人一眼就看明白了,不懂的看注釋也懂了,我就不墨跡了?,F在總算是把消息轉發前的邏輯繞回來構成閉環了。
上一節中提到 class_getMethodImplementation 函數的實現,在查找不到 IMP 時返回 _objc_msgForward,而 _objc_msgForward_stret 正好對應著 class_getMethodImplementation_stret:
IMP class_getMethodImplementation_stret(Class cls, SEL sel)
{
IMP imp = class_getMethodImplementation(cls, sel);
// Translate forwarding function to struct-returning version
if (imp == (IMP)&_objc_msgForward /* not _internal! */) {
return (IMP)&_objc_msgForward_stret;
}
return imp;
}
也就是說 _objc_msgForward* 系列本質都是函數指針,都用匯編語言實現,都可以與 IMP 類型的值作比較。_objc_msgForward 和 _objc_msgForward_stret 聲明在 message.h 文件中。_objc_msgForward_impcache
在早期版本的 Runtime 中叫做 _objc_msgForward_internal
。
objc_msgForward 也只是個入口
從匯編源碼可以很容易看出 **_objc_msgForward 和 _objc_msgForward_stret 會分別調用_objc_forward_handler 和 _objc_forward_handler_stret
**:
ENTRY __objc_msgForward
// Non-stret version
movq __objc_forward_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward
ENTRY __objc_msgForward_stret
// Struct-return version
movq __objc_forward_stret_handler(%rip), %r11
jmp *%r11
END_ENTRY __objc_msgForward_stret
這兩個 handler 函數的區別從字面上就能看出來,不再贅述。
也就是說,消息轉發過程是現將 _objc_msgForward_impcache 強轉成 _objc_msgForward 或 _objc_msgForward_stret,再分別調用 _objc_forward_handler 或 _objc_forward_handler_stret
。
objc_setForwardHandler 設置了消息轉發的回調
在 Objective-C 2.0 之前,默認的 _objc_forward_handler 或 _objc_forward_handler_stret 都是 nil
,而新版本的默認實現是這樣的:
// Default forward handler halts the process.
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret
objc_defaultForwardStretHandler(id self, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif
objc_defaultForwardHandler 中的 _objc_fatal 作用就是打日志并調用 __builtin_trap() 觸發 crash,可以看到我們最熟悉的那句 “unrecognized selector sent to instance” 日志。__builtin_trap() 在殺掉進程的同時還能生成日志,比調用 exit() 更好。objc_defaultForwardStretHandler
就是裝模作樣搞個形式主義,把 objc_defaultForwardHandler 包了一層。attribute((noreturn)) 屬性通知編譯器函數從不返回值,當遇到類似函數需要返回值而卻不可能運行到返回值處就已經退出來的情況,該屬性可以避免出現錯誤信息。這里正適合此屬性,因為要求返回結構體噠。
因為默認的 Handler 干的事兒就是打日志觸發 crash,我們想要實現消息轉發,就需要替換掉 Handler 并賦值給 _objc_forward_handler 或 _objc_forward_handler_stret,賦值的過程就需要用到 objc_setForwardHandler 函數,實現也是簡單粗暴,就是賦值?。?/p>
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
總結
我將整個實現流程繪制出來,過濾了一些不會進入的分支路徑和跟主題無關的細節:
介于國內關于這塊知識的好多文章描述不夠準確和詳細,或是對消息轉發的原理描述理解不夠深刻,或是側重貼源碼而欠思考,所以我做了一個比較全面詳細的講解。
參考文獻
Why objc_msgSend Must be Written in Assembly
Hmmm, What’s that Selector?
A Look Under the Hood of objc_msgSend()
Printing Objective-C Invocations in LLDB
轉載鏈接
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/