消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 IMP 的過程,有了函數(shù)指針就可以執(zhí)行對應(yīng)的方法實現(xiàn);消息轉(zhuǎn)發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉(zhuǎn)發(fā)流程的慢速通道,如果不作轉(zhuǎn)發(fā)處理,則會打日志和拋出異常。
本文不講述開發(fā)者在消息發(fā)送和轉(zhuǎn)發(fā)流程中需要做的事,而是講述原理。能夠很好地閱讀本文的前提是你對Objective-C Runtime已經(jīng)有一定的了解,關(guān)于什么是消息,Class 的結(jié)構(gòu),selector、IMP、元類等概念將不再贅述。本文用到的源碼為 objc4-680 和 CF-1153.18,逆向 CoreFoundation.framework 的系統(tǒng)版本為 macOS 10.11.5,匯編語言架構(gòu)為 x86_64。
此函數(shù)是消息發(fā)送必經(jīng)之路,但只要一提objc_msgSend,都會說它的偽代碼如下或類似的邏輯,反正就是獲取 IMP 并調(diào)用:
id objc_msgSend(id self, SEL _cmd, ...) {
Classclass=object_getClass(self);
IMP imp = class_getMethodImplementation(class,_cmd);
returnimp ? imp(self, _cmd, ...) :0;
}
為啥老用偽代碼?因為objc_msgSend是用匯編語言寫的,針對不同架構(gòu)有不同的實現(xiàn)。如下為x86_64架構(gòu)下的源碼,可以在objc-msg-x86_64.s文件中找到,關(guān)鍵代碼如下:
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宏,判斷被發(fā)送消息的對象是否為nil的。如果為nil,那就直接返回nil。這就是為啥也可以對nil發(fā)消息。
GetIsaFast宏可以『快速地』獲取到對象的isa指針地址(放到r11寄存器,r10會被重寫;在 arm 架構(gòu)上是直接賦值到r9)
CacheLookup這個宏是在類的緩存中查找 selector 對應(yīng)的 IMP(放到r10)并執(zhí)行。如果緩存沒中,那就得到 Class 的方法表中查找了。
MethodTableLookup宏是重點,負責在緩存沒命中時在方法表中負責查找 IMP:
.macroMethodTableLookup
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函數(shù),并將 IMP 返回(從r11挪到rax)。最后在objc_msgSend中調(diào)用 IMP。
其實在objc-msg-x86_64.s中包含了多個版本的objc_msgSend方法,它們是根據(jù)返回值的類型和調(diào)用者的類型分別處理的:
objc_msgSendSuper:向父類發(fā)消息,返回值類型為id
objc_msgSend_fpret:返回值類型為 floating-point,其中包含objc_msgSend_fp2ret入口處理返回值類型為long double的情況
objc_msgSend_stret:返回值為結(jié)構(gòu)體
objc_msgSendSuper_stret:向父類發(fā)消息,返回值類型為結(jié)構(gòu)體
當需要發(fā)送消息時,編譯器會生成中間代碼,根據(jù)情況分別調(diào)用objc_msgSend,objc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret其中之一。
這也是為什么objc_msgSend要用匯編語言而不是 OC、C 或 C++ 語言來實現(xiàn),因為單獨一個方法定義滿足不了多種類型返回值,有的方法返回id,有的返回int。除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便,因為找到 IMP 地址后參數(shù)都在棧上。要是用 C++ 傳遞可變參數(shù)那就悲劇了,prologue 機制會弄亂地址(比如 i386 上為了存儲ebp向后移位 4byte),最后還要用 epilogue 打掃戰(zhàn)場。此外還好考慮不同類型參數(shù)排列組合映射不同方法簽名(method signature)的問題,那 switch 語句得老長了。。。而且匯編程序執(zhí)行效率高,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的。
使用 lookUpImpOrForward 快速查找 IMP
上一節(jié)中說到的_class_lookupMethodAndLoadCache3函數(shù)其實只是簡單的調(diào)用了lookUpImpOrForward函數(shù):
IMP _class_lookupMethodAndLoadCache3(idobj, SEL sel, Class cls)
{
returnlookUpImpOrForward(cls, sel, obj,
YES/*initialize*/,NO/*cache*/,YES/*resolver*/);
}
注意lookUpImpOrForward調(diào)用時使用緩存參數(shù)傳入為NO,因為之前已經(jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)實現(xiàn)了一套查找 IMP 的標準路徑,也就是在消息轉(zhuǎn)發(fā)(Forward)之前的邏輯。
先對 debug 模式下的 assert 進行 unlock:
runtimeLock.assertUnlocked();
runtimeLock本質(zhì)上是對 Darwin 提供的線程讀寫鎖pthread_rwlock_t的一層封裝,提供了一些便捷的方法。
lookUpImpOrForward接著做了如下兩件事:
如果使用緩存(cache參數(shù)為YES),那就調(diào)用cache_getImp方法從緩存查找 IMP。cache_getImp是用匯編語言寫的,也可以在objc-msg-x86_64.s找到,其依然用了之前說過的CacheLookup宏。因為_class_lookupMethodAndLoadCache3調(diào)用lookUpImpOrForward時cache參數(shù)為NO,這步直接略過。
如果是第一次用到這個類且initialize參數(shù)為YES(initialize && !cls->isInitialized()),需要進行初始化工作,也就是開辟一個用于讀寫數(shù)據(jù)的空間。先對runtimeLock寫操作加鎖,然后調(diào)用cls的initialize方法。如果sel == initialize也沒關(guān)系,雖然initialize還會被調(diào)用一次,但不會起作用啦,因為cls->isInitialized()已經(jīng)是YES啦。
考慮到運行時類中的方法可能會增加,需要先做讀操作加鎖,使得方法查找和緩存填充成為原子操作。添加 category 會刷新緩存,之后如果舊數(shù)據(jù)又被重填到緩存中,category 添加操作就會被忽略掉。
runtimeLock.read();
之后的邏輯整理如下:
如果 selector 是需要被忽略的垃圾回收用到的方法,則將 IMP 結(jié)果設(shè)為_objc_ignored_method,這是個匯編程序入口,可以理解為一個標記。對此種情況進行緩存填充操作后,跳到第 7 步;否則執(zhí)行下一步。
查找當前類中的緩存,跟之前一樣,使用cache_getImp匯編程序入口。如果命中緩存獲取到了 IMP,則直接跳到第 7 步;否則執(zhí)行下一步。
在當前類中的方法列表(method list)中進行查找,也就是根據(jù) selector 查找到 Method 后,獲取 Method 中的 IMP(也就是method_imp屬性),并填充到緩存中。查找過程比較復(fù)雜,會針對已經(jīng)排序的列表使用二分法查找,未排序的列表則是線性遍歷。如果成功查找到 Method 對象,就直接跳到第 7 步;否則執(zhí)行下一步。
在繼承層級中遞歸向父類中查找,情況跟上一步類似,也是先查找緩存,緩存沒中就查找方法列表。這里跟上一步不同的地方在于緩存策略,有個_objc_msgForward_impcache匯編程序入口作為緩存中消息轉(zhuǎn)發(fā)的標記。也就是說如果在緩存中找到了 IMP,但如果發(fā)現(xiàn)其內(nèi)容是_objc_msgForward_impcache,那就終止在類的繼承層級中遞歸查找,進入下一步;否則跳到第 7 步。
當傳入lookUpImpOrForward的參數(shù)resolver為YES并且是第一次進入第 5 步時,時進入動態(tài)方法解析;否則進入下一步。這步消息轉(zhuǎn)發(fā)前的最后一次機會。此時釋放讀入鎖(runtimeLock.unlockRead()),接著間接地發(fā)送+resolveInstanceMethod或+resolveClassMethod消息。這相當于告訴程序員『趕緊用 Runtime 給類里這個 selector 弄個對應(yīng)的 IMP 吧』,因為此時鎖已經(jīng) unlock 了所以不會緩存結(jié)果,甚至還需要軟性地處理緩存過期問題可能帶來的錯誤。這里的業(yè)務(wù)邏輯稍微復(fù)雜些,后面會總結(jié)。因為這些工作都是在非線程安全下進行的,完成后需要回到第 1 步再次查找 IMP。
此時不僅沒查找到 IMP,動態(tài)方法解析也不奏效,只能將_objc_msgForward_impcache當做 IMP 并寫入緩存。這也就是之前第 4 步中為何查找到_objc_msgForward_impcache就表明了要進入消息轉(zhuǎn)發(fā)了。
讀操作解鎖,并將之前找到的 IMP 返回。(無論是正經(jīng) IMP 還是不正經(jīng)的_objc_msgForward_impcache)這步還偏執(zhí)地做了一些腦洞略大的 assert,很有趣。
對于第 5 步,其實是直接調(diào)用_class_resolveMethod函數(shù),在這個函數(shù)中實現(xiàn)了復(fù)雜的方法解析邏輯。如果cls是元類則會發(fā)送+resolveClassMethod,然后根據(jù)lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)函數(shù)的結(jié)果來判斷是否發(fā)送+resolveInstanceMethod;如果不是元類,則只需要發(fā)送+resolveInstanceMethod消息。這里調(diào)用+resolveInstanceMethod或+resolveClassMethod時再次用到了objc_msgSend,而且第三個參數(shù)正是傳入lookUpImpOrForward的那個sel。在發(fā)送方法解析消息之后還會調(diào)用lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)來判斷是否已經(jīng)添加上sel對應(yīng)的 IMP 了,打印出結(jié)果。
最后lookUpImpOrForward方法也會把真正的 IMP 或者需要消息轉(zhuǎn)發(fā)的_objc_msgForward_impcache返回,并最終專遞到objc_msgSend中。而_objc_msgForward_impcache會在轉(zhuǎn)化成_objc_msgForward或_objc_msgForward_stret。這個后面會講解原理。
回過頭來會發(fā)現(xiàn)objc_msgSend的偽代碼描述得很傳神啊,因為class_getMethodImplementation的實現(xiàn)如下:
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if(!cls? ||? !sel)returnnil;
imp = lookUpImpOrNil(cls, sel,nil,YES/*initialize*/,YES/*cache*/,YES/*resolver*/);
// Translate forwarding function to C-callable external version
if(!imp) {
return_objc_msgForward;
}
returnimp;
}
lookUpImpOrNil函數(shù)獲取不到 IMP 時就返回_objc_msgForward,后面會講到它。lookUpImpOrNil跟lookUpImpOrForward的功能很相似,只是將lookUpImpOrForward實現(xiàn)中的_objc_msgForward_impcache替換成了nil:
IMP lookUpImpOrNil(Class cls, SEL sel, idinst,
boolinitialize,boolcache,boolresolver)
{
IMP imp = lookUpImpOrForward(cls, sel,inst,initialize,cache,resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
lookUpImpOrNil方法可以查找到 selector 對應(yīng)的 IMP 或是nil,所以如果不考慮返回值類型為結(jié)構(gòu)體的情況,用那幾行偽代碼來表示復(fù)雜的匯編實現(xiàn)還是挺恰當?shù)摹?/p>
forwarding中路漫漫的消息轉(zhuǎn)發(fā)
objc_msgForward_impcache 的轉(zhuǎn)換
_objc_msgForward_impcache只是個內(nèi)部的函數(shù)指針,只存儲于上節(jié)提到的類的方法緩存中,需要被轉(zhuǎn)化為_objc_msgForward和_objc_msgForward_stret才能被外部調(diào)用。但在Mac OS XmacOS 10.6 及更早版本的 libobjc.A.dylib 中是不能直接調(diào)用的,況且我們根本不會直接用到它。帶stret后綴的函數(shù)依舊是返回值為結(jié)構(gòu)體的版本。
上一節(jié)最后講到如果沒找到 IMP,就會將_objc_msgForward_impcache返回到objc_msgSend函數(shù),而正是因為它是用匯編語言寫的,所以將內(nèi)部使用的_objc_msgForward_impcache轉(zhuǎn)化成外部可調(diào)用的_objc_msgForward或_objc_msgForward_stret也是由匯編代碼來完成。實現(xiàn)原理很簡單,就是增加個靜態(tài)入口__objc_msgForward_impcache,然后根據(jù)此時 CPU 的狀態(tài)寄存器的內(nèi)容來決定轉(zhuǎn)換成哪個。如果是NE(Not Equal) 則轉(zhuǎn)換成_objc_msgForward_stret,反之是EQ(Equal) 則轉(zhuǎn)換成_objc_msgForward:
jne__objc_msgForward_stret
jmp__objc_msgForward
為何根據(jù)狀態(tài)寄存器的值來判斷轉(zhuǎn)換成哪個函數(shù)指針呢?回過頭來看看objc_msgSend中調(diào)用完MethodTableLookup之后干了什么:
MethodTableLookup %a1, %a2 // r11 = IMP
cmp%r11, %r11// set eq (nonstret)forforwarding
jmp*%r11//goto*imp
再看看返回值為結(jié)構(gòu)體的objc_msgSend_stret這里的邏輯:
MethodTableLookup%a2,%a3// r11 = IMP
test%r11,%r11// set ne (stret) for forward; r11!=0
jmp*%r11// goto *imp
稍微懂匯編的人一眼就看明白了,不懂的看注釋也懂了,我就不墨跡了。現(xiàn)在總算是把消息轉(zhuǎn)發(fā)前的邏輯繞回來構(gòu)成閉環(huán)了。
上一節(jié)中提到class_getMethodImplementation函數(shù)的實現(xiàn),在查找不到 IMP 時返回_objc_msgForward,而_objc_msgForward_stret正好對應(yīng)著class_getMethodImplementation_stret:
IMPclass_getMethodImplementation_stret(Class cls,SELsel)
{
IMP imp = class_getMethodImplementation(cls,sel);
// Translate forwardingfunctiontostruct-returningversion
if(imp == (IMP)&_objc_msgForward/* not _internal! */) {
return (IMP)&_objc_msgForward_stret;
}
return imp;
}
也就是說_objc_msgForward*系列本質(zhì)都是函數(shù)指針,都用匯編語言實現(xiàn),都可以與 IMP 類型的值作比較。_objc_msgForward和_objc_msgForward_stret聲明在message.h文件中。_objc_msgForward_impcache在早期版本的 Runtime 中叫做_objc_msgForward_internal。
從匯編源碼可以很容易看出_objc_msgForward和_objc_msgForward_stret會分別調(diào)用_objc_forward_handler和_objc_forward_handler_stret:
ENTRY__objc_msgForward
// Non-stretversion
movq__objc_forward_handler(%rip), %r11
jmp*%r11
END_ENTRY__objc_msgForward
ENTRY__objc_msgForward_stret
//Struct-returnversion
movq__objc_forward_stret_handler(%rip), %r11
jmp*%r11
END_ENTRY__objc_msgForward_stret
這兩個 handler 函數(shù)的區(qū)別從字面上就能看出來,不再贅述。
也就是說,消息轉(zhuǎn)發(fā)過程是現(xiàn)將_objc_msgForward_impcache強轉(zhuǎn)成_objc_msgForward或_objc_msgForward_stret,再分別調(diào)用_objc_forward_handler或_objc_forward_handler_stret。
objc_setForwardHandler 設(shè)置了消息轉(zhuǎn)發(fā)的回調(diào)
在 Objective-C 2.0 之前,默認的_objc_forward_handler或_objc_forward_handler_stret都是nil,而新版本的默認實現(xiàn)是這樣的:
// Default forward handler halts the process.
__attribute__((noreturn))void
objc_defaultForwardHandler(idself, 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
structstret {inti[100]; };
__attribute__((noreturn))structstret
objc_defaultForwardStretHandler(idself, SEL sel)
{
objc_defaultForwardHandler(self, sel);
}
void*_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif
objc_defaultForwardHandler中的_objc_fatal作用就是打日志并調(diào)用__builtin_trap()觸發(fā) crash,可以看到我們最熟悉的那句 “unrecognized selector sent to instance” 日志。__builtin_trap()在殺掉進程的同時還能生成日志,比調(diào)用exit()更好。objc_defaultForwardStretHandler就是裝模作樣搞個形式主義,把objc_defaultForwardHandler包了一層。__attribute__((noreturn))屬性通知編譯器函數(shù)從不返回值,當遇到類似函數(shù)需要返回值而卻不可能運行到返回值處就已經(jīng)退出來的情況,該屬性可以避免出現(xiàn)錯誤信息。這里正適合此屬性,因為要求返回結(jié)構(gòu)體噠。
因為默認的 Handler 干的事兒就是打日志觸發(fā) crash,我們想要實現(xiàn)消息轉(zhuǎn)發(fā),就需要替換掉 Handler 并賦值給_objc_forward_handler或_objc_forward_handler_stret,賦值的過程就需要用到objc_setForwardHandler函數(shù),實現(xiàn)也是簡單粗暴,就是賦值啊:
voidobjc_setForwardHandler(void*fwd,void*fwd_stret)
{
_objc_forward_handler = fwd;
#ifSUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
重頭戲在于對objc_setForwardHandler的調(diào)用,以及之后的消息轉(zhuǎn)發(fā)調(diào)用棧。這回不是在 Objective-C Runtime (libobjc.dylib)中啦,而是在 Core Foundation(CoreFoundation.framework)中。雖然 CF 是開源的,但有意思的是蘋果故意在開源的代碼中刪除了在CFRuntime.c文件__CFInitialize()中調(diào)用objc_setForwardHandler的代碼。__CFInitialize()函數(shù)是在 CF runtime 連接到進程時初始化調(diào)用的。從反編譯得到的匯編代碼中可以很容易跟 C 源碼對比出來,我用紅色標出了同一段代碼的差異。
匯編語言還是比較好理解的,紅色標出的那三個指令就是把__CF_forwarding_prep_0和___forwarding_prep_1___作為參數(shù)調(diào)用objc_setForwardHandler方法(那么之前那兩個 DefaultHandler 卵用都沒有咯,反正不出意外會被 CF 替換掉):
然而在源碼中對應(yīng)的代碼卻被刪掉啦:
蘋果提供的 __CFInitialize() 函數(shù)源碼
在早期版本的 CF 源碼中,還是可以看到__CF_forwarding_prep_0和___forwarding_prep_1___的聲明的,但是不會有實現(xiàn)源碼,也沒有對objc_setForwardHandler的調(diào)用。這些細節(jié)從函數(shù)調(diào)用棧中無法看出,只能逆向工程看匯編指令。但從函數(shù)調(diào)用棧可以看出__CF_forwarding_prep_0和___forwarding_prep_1___這兩個 Forward Handler 做了啥:
2016-06-14 12:50:15.385 MessageForward[67364:7174239] -[MFObject sendMessage]: unrecognized selector sent toinstance0x1006001a0
2016-06-14 12:50:15.387 MessageForward[67364:7174239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MFObject sendMessage]: unrecognized selector sent toinstance0x1006001a0'
*** Firstthrowcall stack:
(
0? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8fa554f2 __exceptionPreprocess + 178
1? libobjc.A.dylib? ? ? ? ? ? ? ? ? ? 0x00007fff98396f7e objc_exception_throw + 48
2? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8fabf1ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8f9c5571 ___forwarding___ + 1009
4? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x00007fff8f9c50f8 _CF_forwarding_prep_0 + 120
5? MessageForward? ? ? ? ? ? ? ? ? ? ? 0x0000000100000f1f main + 79
6? libdyld.dylib? ? ? ? ? ? ? ? ? ? ? 0x00007fff8bc2c5ad start + 1
7? ???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
這個日志場景熟悉得不能再熟悉了,可以看出_CF_forwarding_prep_0函數(shù)調(diào)用了___forwarding___函數(shù),接著又調(diào)用了doesNotRecognizeSelector方法,最后拋出異常。但是靠這些是無法說服看客的,還得靠逆向工程反編譯后再反匯編成偽代碼來一探究竟,刨根問底。
__CF_forwarding_prep_0和___forwarding_prep_1___函數(shù)都調(diào)用了___forwarding___,只是傳入?yún)?shù)不同。___forwarding___有兩個參數(shù),第一個參數(shù)為將要被轉(zhuǎn)發(fā)消息的棧指針(可以簡單理解成 IMP),第二個參數(shù)標記是否返回結(jié)構(gòu)體。__CF_forwarding_prep_0第二個參數(shù)傳入0,___forwarding_prep_1___傳入的是1,從函數(shù)名都能看得出來。下面是這兩個函數(shù)的偽代碼:
int__CF_forwarding_prep_0(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {
rax= ____forwarding___(rsp,0x0);
if (rax!=0x0) { // 轉(zhuǎn)發(fā)結(jié)果不為空,將內(nèi)容返回
rax= *rax;
}
else { // 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 objc_msgSend(id self, SEL _cmd,...);
rsi= *(rsp+0x8);
rdi= *rsp;
rax= objc_msgSend(rdi,rsi);
}
returnrax;
}
int___forwarding_prep_1___(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {
rax= ____forwarding___(rsp,0x1);
if (rax!=0x0) {// 轉(zhuǎn)發(fā)結(jié)果不為空,將內(nèi)容返回
rax= *rax;
}
else {// 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 objc_msgSend_stret(void * st_addr, id self, SEL _cmd, ...);
rdx= *(rsp+0x10);
rsi= *(rsp+0x8);
rdi= *rsp;
rax= objc_msgSend_stret(rdi,rsi,rdx);
}
returnrax;
}
在x86_64架構(gòu)中,rax寄存器一般是作為返回值,rsp寄存器是棧指針。在調(diào)用objc_msgSend函數(shù)時,參數(shù)arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5分別使用寄存器rdi, rsi, rdx, rcx, r8, r9的值。在調(diào)用objc_msgSend_stret時第一個參數(shù)為st_addr,其余參數(shù)依次后移。為了能夠打包出NSInvocation實例并傳入后續(xù)的forwardInvocation:方法,在調(diào)用___forwarding___函數(shù)之前會先將所有參數(shù)壓入棧中。因為寄存器rsp為棧指針指向棧頂,所以rsp的內(nèi)容就是self啦,因為x86_64是小端,棧增長方向是由高地址到低地址,所以從棧頂往下移動一個指針需要加0x8(64bit)。而將參數(shù)入棧的順序是從后往前的,也就是說arg0是最后一個入棧的,位于棧頂:
__CF_forwarding_prep_0:
0000000000085080pushrbp; XREF=___CFInitialize+138
0000000000085081movrbp,rsp
0000000000085084subrsp,0xd0
000000000008508bmovqword[ss:rsp+0xb0],rax
0000000000085093movqqword[ss:rsp+0xa0],xmm7
000000000008509cmovqqword[ss:rsp+0x90],xmm6
00000000000850a5movqqword[ss:rsp+0x80],xmm5
00000000000850aemovqqword[ss:rsp+0x70],xmm4
00000000000850b4movqqword[ss:rsp+0x60],xmm3
00000000000850bamovqqword[ss:rsp+0x50],xmm2
00000000000850c0movqqword[ss:rsp+0x40],xmm1
00000000000850c6movqqword[ss:rsp+0x30],xmm0
00000000000850ccmovqword[ss:rsp+0x28],r9
00000000000850d1movqword[ss:rsp+0x20],r8
00000000000850d6movqword[ss:rsp+0x18],rcx
00000000000850dbmovqword[ss:rsp+0x10],rdx
00000000000850e0movqword[ss:rsp+0x8],rsi
00000000000850e5movqword[ss:rsp],rdi
00000000000850e9movrdi,rsp; argument #1 for method ____forwarding___
00000000000850ecmovrsi,0x0; argument #2 for method ____forwarding___
00000000000850f3call____forwarding___
消息轉(zhuǎn)發(fā)的邏輯幾乎都寫在___forwarding___函數(shù)中了,實現(xiàn)比較復(fù)雜,反編譯出的偽代碼也不是很直觀。我對arigrant.com的結(jié)果完善如下:
int __forwarding__(void*frameStackPointer, int isStret) {
id receiver = *(id*)frameStackPointer;
SEL sel = *(SEL*)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 調(diào)用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget&&forwarding != receiver) {
if (isStret==1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 僵尸對象
const char *className = class_getName(receiverClass);
const char *zombiePrefix ="_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) ==0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
}
// 調(diào)用 methodSignatureForSelector 獲取方法簽名后再調(diào)用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.? Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// selector 是否已經(jīng)在 Runtime 注冊過
if (sel!= registeredSel) {
CFLog(kCFLogLevelWarning,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(),9);
}
這么一大坨代碼就是整個消息轉(zhuǎn)發(fā)路徑的邏輯,概括如下:
先調(diào)用forwardingTargetForSelector方法獲取新的 target 作為 receiver 重新執(zhí)行 selector,如果返回的內(nèi)容不合法(為nil或者跟舊 receiver 一樣),那就進入第二步。
調(diào)用methodSignatureForSelector獲取方法簽名后,判斷返回類型信息是否正確,再調(diào)用forwardInvocation執(zhí)行NSInvocation對象,并將結(jié)果返回。如果對象沒實現(xiàn)methodSignatureForSelector方法,進入第三步。
調(diào)用doesNotRecognizeSelector方法。
doesNotRecognizeSelector之前其實還有個判斷 selector 在 Runtime 中是否注冊過的邏輯,但在我們正常發(fā)消息的時候不會出此問題。但如果手動創(chuàng)建一個NSInvocation對象并調(diào)用invoke,并將第二個參數(shù)設(shè)置成一個不存在的 selector,那就會導致這個問題,并輸入日志 “does not match selector known to Objective C runtime”。較真兒的讀者可能會有疑問:何這段邏輯判斷干脆用不到卻還存在著?難道除了__CF_forwarding_prep_0和___forwarding_prep_1___函數(shù)還有其他函數(shù)也調(diào)用___forwarding___么?莫非消息轉(zhuǎn)發(fā)還有其他路徑?其實并不是!原因是___forwarding___調(diào)用了___invoking___函數(shù),所以上面的偽代碼直接把___invoking___函數(shù)的邏輯也『翻譯』過來了。除了___forwarding___函數(shù),以下方法也會調(diào)用___invoking___函數(shù):
-[NSInvocation invoke]
-[NSInvocationinvokeUsingIMP:]
-[NSInvocation invokeSuper]
doesNotRecognizeSelector方法其實在 libobj.A.dylib 中已經(jīng)廢棄了,而是在 CF 框架中實現(xiàn),而且也不是開源的。從函數(shù)調(diào)用棧可以發(fā)現(xiàn)doesNotRecognizeSelector之后會拋出異常,而 Runtime 中廢棄的實現(xiàn)知識打日志后直接殺掉進程(__builtin_trap())。下面是 CF 中實現(xiàn)的偽代碼:
void-[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {
r14 = ___CFFullMethodName([selfclass],self, arg2);
_CFLog(0x3,@"%@: unrecognized selector sent to instance %p", r14,self, r8, r9, stack[2048]);
rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault,0x0,@"%@: unrecognized selector sent to instance %p"));
if(*(int8_t *)___CFOASafe!=0x0) {
___CFRecordAllocationEvent();
}
rax = _objc_rootAutorelease(rbx);
rax = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];
objc_exception_throw(rax);
return;
}
void+[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {
r14 = ___CFFullMethodName([selfclass],self, arg2);
_CFLog(0x3,@"%@: unrecognized selector sent to class %p", r14,self, r8, r9, stack[2048]);
rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault,0x0,@"%@: unrecognized selector sent to class %p"));
if(*(int8_t *)___CFOASafe!=0x0) {
___CFRecordAllocationEvent();
}
rax = _objc_rootAutorelease(rbx);
rax = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];
objc_exception_throw(rax);
return;
}
也就是說我們可以 overridedoesNotRecognizeSelector或者捕獲其拋出的異常。在這里還是大有文章可做的。
我將整個實現(xiàn)流程繪制出來,過濾了一些不會進入的分支路徑和跟主題無關(guān)的細節(jié):
介于國內(nèi)關(guān)于這塊知識的好多文章描述不夠準確和詳細,或是對消息轉(zhuǎn)發(fā)的原理描述理解不夠深刻,或是側(cè)重貼源碼而欠思考,所以我做了一個比較全面詳細的講解。
Why objc_msgSend Must be Written in Assembly