關于OC中的消息發(fā)送的實現(xiàn),在去年也看過一次,當時有點不太理解,但是今年再看卻很容易理解。?
我想這跟知識體系的構建有關,如果你不認識有磚、水泥等這些建筑的基本組成部分,那么我們應該很難理解建筑是怎么建造出來的吧??
學習新知識,應該也是同樣的道理!
今年再看 消息發(fā)送機制時,也翻了很多文章,本來想自己總結一遍的,但是感覺這篇?Objective-C 消息發(fā)送與轉發(fā)機制原理?實在寫的太好了,就直接轉載了。?
原文:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/
消息發(fā)送和轉發(fā)流程可以概括為:消息發(fā)送(Messaging)是Runtime通過selector 快速查找IMP的過程,有了函數(shù)指針就可以執(zhí)行對應的方法實現(xiàn);消息轉發(fā)(Message Frowarding)是在查找IMP失敗后一系列轉發(fā)流程的慢速通道,如果不做轉發(fā)處理,則會打日志和拋出異常。
本文不講述開發(fā)者在消息發(fā)送和轉發(fā)流程中需要做的事,而是講述原理。能夠很好地閱讀本文的前提是你對?Objective-C Runtime?已經(jīng)有一定的了解,關于什么是消息,Class的結構,Selector、IMP、元類等概念將不再贅述。本文用到的源碼為objc-680 和 CF-1153.18,逆向CoreFoundation.framework的系統(tǒng)版本為macOS10.11.5,匯編語言架構為x86_64。
此函數(shù)是消息發(fā)送必經(jīng)之路,但只要一提到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使用匯編語言寫的,針對不同架構有不同的實現(xiàn)(我們可以在objc-680的Source目錄下看到多個objc-msg-xxxx的匯編實現(xiàn)文件)。如下為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宏,判斷被發(fā)送消息的對象是否為nil的。如果為nil,那就直接返回nil。這就是為啥也可以對?nil發(fā)消息。?
GetIsaFast宏可以【快速地】獲取到對象的isa指針地址(放到r11寄存器,r10會被重寫;在arm架構上是直接賦值到r9)。?
CacheLookup這個宏是在類的緩存中查找selector對應的IMP(放到r10)并執(zhí)行。如果緩存沒中,那就得到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函數(shù),并將IMP返回(從r11挪到rax)。最后在objc_msgSend中調用IMP。
為什么使用匯編語言
其實在objc-msg-x86_64.s中包含了多個版本的?objc_msgSend方法,它們是根據(jù)返回值的類型和調用者的類型分別處理的:?
*?objc_msgSendSuper:向父類發(fā)消息,返回值類型為 id?
*?objc_msgSend_fpret:返回值類型為 floating-point,其中包含 objc_msgSend_fp2ret 入口處理返回值類型為 long double 的情況?
*?objc_msgSend_stret:返回值為結構體?
*?objc_msgSendSuper_stret:向父類發(fā)消息,返回值類型為結構體?
當需要發(fā)送消息時,編譯器會生成中間代碼,根據(jù)情況分別調用objc_msgSend,?objc_msgSend_stret,?objc_msgSendSuper, 或?objc_msgSendSuper_stret?其中之一。
這也是為什么?objc_msgSend?要用匯編語言而不是 OC、C或C++語言來實現(xiàn),因為單獨的一個方法滿足不了多種類型返回值,有的方法返回?id,有的返回?int.考慮到不同類型參數(shù)返回值排列組合映射不同方法簽名(method signature)的問題,那switch語句得老長了。。。這些原因可以總結為?Calling Convention (調用慣例),也就是說函數(shù)調用者與被調用者必須約定好參數(shù)與返回值在不同架構處理器上的存取規(guī)則,比如參數(shù)是以何種順序存儲在棧上,或是存儲在哪些寄存器上。除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便,因為找到IMP地址后參數(shù)都在棧上。要是用C++傳遞可變參數(shù)那就被拒了,prologue機制會弄亂地址(比如i386上為了存儲ebp 向后移位bbyte),最后還要用epilogue打掃戰(zhàn)場。而且匯編程序執(zhí)行效率高,在Objective-C Runtime中調用頻率較高的函數(shù)好多都用匯編編寫的。
objc_msgSend_fpret?后面fpret 其實是float point return 的縮寫;stret 就是struct return的縮寫,其他同理。?
關于 Calling Convention,可以去看Bang 的文章動態(tài)調用C函數(shù)的 Calling Convention一節(jié)
使用 lookUpImpOrForward 快速查找 IMP
上一節(jié)說到的?_class_lookupMethodAndLoadCache3?函數(shù)其實只是簡單的調用了?lookUpImpOrForward?函數(shù):
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
? ? return lookUpImpOrForward(cls, sel, obj,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意lookUpImpOrForward調用時使用緩存參數(shù)傳入為NO,因為之前已經(jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)?實現(xiàn)了一套查找IMP的標準路徑,也就是在消息轉發(fā)(Forward)之前的邏輯。
優(yōu)化緩存查找&類的初始化
先對debug模式下的assert進行unlock:
runtimeLock.assertUnlocked();
runtimeLock?本質上是對Darwin提供的線程讀寫鎖?pthread_rwlock_t?的一層封裝,提供了一些便捷的方法。
lookUpImpOrForward接著做了如下兩件事:?
1.如果使用緩存(cache參數(shù)為?YES),那就調用?cache_getImp方法從緩存查找IMP。cache_getImp?是用匯編語言寫的,也可以在?objc-msg-x86_64.s中找到,其依然用了之前說過的?CacheLookup?宏。因為?_class_lookupMethodAndLoadCache3?調用?lookUpImpOrForward?時,cache?參數(shù)為?NO,這步直接略過。?
2.如果是第一次用到這個類且?initialize?參數(shù)為?YES(initialize && !cls->isInitialized()),需要進行初始化工作,也就是開辟一個用于讀寫數(shù)據(jù)的空間。先對?runtimeLock?寫操作加鎖,然后調用?cls?的?initialize?方法。如果?sel == initialize?也沒關系,雖然?initialize?還會被調用一次,但不會起作用啦,因為?cls->isInitialized()?已經(jīng)是 YES 啦。
繼續(xù)在類的繼承體系中查找
考慮到運行時類中的方法可能會增加,需要先做讀操作加鎖,使得方法查找和緩存填充成原子操作。添加category 會刷新緩存,之后如果舊數(shù)據(jù)又被重填到緩存中,category 添加操作就會被忽略掉。
runtimeLock.read();
之后的邏輯整理如下:?
1.如果 selector 是需要被忽略的垃圾回收用到的方法,則將 IMP 結果設為?_objc_ignored_method,這是個匯編程序入口,可以理解為一個標記。對此種情況進行緩存填充操作后,跳到第 7 步;否則執(zhí)行下一步。?
2.查找當前類中的緩存,跟之前一樣,使用?cache_getImp?匯編程序入口。如果命中緩存獲取到了 IMP,則直接跳到第 7 步;否則執(zhí)行下一步。?
3.在當前類中的方法列表(method list)中進行查找,也就是根據(jù) selector 查找到 Method 后,獲取 Method 中的 IMP(也就是?method_imp?屬性),并填充到緩存中。查找過程比較復雜,會針對已經(jīng)排序的列表使用二分法查找,未排序的列表則是線性遍歷。如果成功查找到 Method 對象,就直接跳到第 7 步;否則執(zhí)行下一步。?
4.在繼承層級中遞歸向父類中查找,情況跟上一步類似,也是先查找緩存,緩存沒中就查找方法列表。這里跟上一步不同的地方在于緩存策略,有個?_objc_msgForward_impcache?匯編程序入口作為緩存中消息轉發(fā)的標記。也就是說如果在緩存中找到了 IMP,但如果發(fā)現(xiàn)其內容是?_objc_msgForward_impcache,那就終止在類的繼承層級中遞歸查找,進入下一步;否則跳到第 7 步。?
5.當傳入lookUpImpOrForward的參數(shù)resolver為YES并且是第一次進入第5步時,進入動態(tài)方法解析;否則進入下一步。這步是消息轉發(fā)前的最后一次機會。此時釋放讀入鎖(runtimeLock.unlockRead()),接著間接地發(fā)送+resolveInstanceMethod或+resolveClassMethod消息。這相當于告訴程序員『趕緊用 Runtime 給類里這個 selector 弄個對應的 IMP 吧』,因為此時鎖已經(jīng)unlock了所以不會緩存結果,甚至還需要軟性地處理緩存過期問題可能帶來的錯誤。這里的業(yè)務邏輯稍微復雜些,后面會總結。因為這些工作都是在非線程安全下進行的,完成后需要回到第1步再次查找IMP.?
6.此時不僅沒查找到IMP,動態(tài)方法解析也不奏效,只能將_objc_msgForward_impcache當做IMP并寫入緩存。這也就是之前第4步中為何查找到_objc_msgForward_impcache就表明了要進入消息轉發(fā)了。?
7.讀操作解鎖,并將之前找到的IMP返回。(無論是正經(jīng)IMP還是不正經(jīng)的_objc_msgForward_impcache)這步還偏執(zhí)地做了一些腦洞略大的assert,很有趣。
對于第5步,其實是直接調用_class_resolveMethod函數(shù),在這個函數(shù)中實現(xiàn)了復雜的方法解析邏輯。如果cls是元類則會發(fā)送+resolveClassMethod,然后根據(jù)lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)函數(shù)的結果來判斷是否發(fā)送+resolveInstanceMethod;如果不是元類,則只需要發(fā)送+resolveInstanceMethod消息。這里調用+resolveInstanceMethod或+resolveClassMethod時,再次用到了objc_msgSend,而且第三個參數(shù)正是傳入lookUpImpOrForward的那個sel。在發(fā)送方法即系消息之后還會調用lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)來判斷是否已經(jīng)添加上sel對應的IMP了,打印出結果。
最后lookUpImpOrForward方法也會把真正的IMP或者需要消息轉發(fā)的_objc_msgForward_impcache返回,并最終傳遞到objc_msgSend中。而_objc_msgForward_impcache會在轉化成_objc_msgForward?或?_objc_msgForward_stret,這個后面會講解原理。
回顧objc_msgSend偽代碼
回過頭來會發(fā)現(xiàn)objc_msgSend的偽代碼描述的很傳神,因為class_getMethodImplementation的實現(xiàn)如下:
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?函數(shù)獲取不到 IMP 時就返回?_objc_msgForward,后面會講到它。lookUpImpOrNil?跟?lookUpImpOrForward?的功能很相似,只是將?lookUpImpOrForward?實現(xiàn)中的?_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,如果不考慮返回值類型為結構體的情況,用那幾行偽代碼來表示復雜的匯編實現(xiàn)還是挺恰當?shù)摹?/p>
forwarding 中路漫漫的消息轉發(fā)
objc_msgForward_impcache 的轉換
_objc_msgForward_impcache只是個內部的函數(shù)指針,只存儲于上節(jié)提到的類的方法緩存中,需要被轉化為_objc_msgForward?和?_objc_msgForward_stret才能被外部調用。但在 macOS 10.6及更早版本的libobjc.A.dylib中是不能直接調用的。況且我們根本不會直接用到它。帶?stret后綴的函數(shù)依舊是返回值為結構體的版本。
上一節(jié)最后降到如果沒找到IMP,就會將_objc_msgForward_impcache返回到objc_msgSend函數(shù),而正是因為它是用匯編語言寫的,所以將內部使用的_objc_msgForward_impcache?轉化成外部可調用的_objc_msgForward?或?_objc_msgForward_stret?也是由匯編代碼來完成。實現(xiàn)原理很簡單,就是增加個靜態(tài)入口__objc_msgForward_impcache,然后根據(jù)此時CPU的狀態(tài)寄存器的內容來決定轉換成哪個。如果是NE(not Equal)則轉換成_objc_msgForward_stret,反之是EQ(Equal)則轉換成_objc_msgForward:
jne __objc_msgForward_stret
jmp __objc_msgForward
為何根據(jù)狀態(tài)寄存器的值來判斷轉換成哪個函數(shù)指針呢?回過頭來看看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
稍微懂變成的人一眼就看明白了,不懂的看注釋也懂了,我就不墨跡了。現(xiàn)在總算是把消息轉發(fā)前的邏輯繞回來構成閉環(huán)了。?
上一節(jié)中提到?class_getMethodImplementation?函數(shù)的實現(xiàn),在查找不到IMP時返回?_objc_msgForward,而_objc_msgForward_stret正好對應著?class_getMethodImplementation_stret:
IMP class_getMethodImplementation_stret(Classcls,SELsel){ 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*系列本質都是函數(shù)指針,都用匯編語言實現(xiàn),都可以與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 函數(shù)的區(qū)別從字面上就能看出來,不再贅述。?
也就是說,消息轉發(fā)過程是先將_objc_msgForward_impcache強轉成?_objc_msgForward或?_objc_msgForward_stret,再分別調用?_objc_forward_handler?或?_objc_forward_handler_stret。
objc_setForwardHandler 設置了消息轉發(fā)的回調
在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(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()?觸發(fā)crash,可以看到我們最熟悉的那句?unrecognized selector sent to instance?日志。?__builtin_trap()?在殺掉進程的同事還能生成日志,比調用?exit()更好。objc_defaultForwardStretHandler就是裝模作樣搞個形式主義,把objc_defaultForwardHandler?包了一層。__attribute__((noreturn))?屬性通知編譯器函數(shù)從不返回值,當遇到類型函數(shù)需要返回值而卻不可能運行到返回值處就已經(jīng)退出來的情況,該屬性可以避免出現(xiàn)錯誤信息。這里正適合此屬性,因為要求返回結構體。
因為默認的Handler干的事兒就是打日志觸發(fā)crash,我們想要實現(xiàn)消息轉發(fā),就需要替換掉Handler并賦值給?_objc_forward_handler?或?_objc_forward_handler_stret,賦值的過程就需要用到?objc_setForwardHandler?函數(shù),實現(xiàn)也是簡單粗暴,就是賦值啊:
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
? ? _objc_forward_handler = fwd;
#ifSUPPORT_STRET? ?
_objc_forward_stret_handler = fwd_stret;
#endif
}
逆向工程助力刨根問底
重頭戲在于對?objc_setForwardHandler?的調用,以及之后的消息轉發(fā)調用棧。這回不是在 Objective-C Runtime (libobjc.dylib)中啦,而是在Core Foundation(CoreFoundation.framework)中。雖然CF是開源的,但有意思的是蘋果故意在開源的代碼中刪除了在?CFRuntime.c?文件?__CFInitialize()?中調用?objc_setForwardHandler?的代碼。__CFInitialize()函數(shù)是在CF Runtime連接到進程時初始化調用的。從反編譯得到的匯編代碼中可以很容易跟 C 源碼對比出來,我用紅色標出了同一段代碼的差異。
匯編語言還是比較好理解的,紅色標出的那三個指令就是把__CF_forwarding_prep_0?和?___forwarding_prep_1___作為參數(shù)調用?objc_setForwardHandler?方法(那么值錢那兩個DefaultHandler 卵用都沒有咯,反正不出意外會被 CF 替換掉):
然而在源碼中對應的代碼卻被刪掉啦:
在早起版本的CF源碼中,還是可以看到?__CF_forwarding_prep_0?和?___forwarding_prep_1___的聲明的,但是不會有實現(xiàn)源碼,也沒有對?objc_setForwardHandler?的調用。這些細節(jié)從函數(shù)調用棧中無法看出,只能逆向工程看匯編指令。但從函數(shù)調用棧可以看出?__CF_forwarding_prep_0?和?___forwarding_prep_1___這兩個Forward Handler做了啥:
2016-06-14 12:50:15.385 MessageForward[67364:7174239] -[MFObject sendMessage]: unrecognized selector sent to instance 0x1006001a0
2016-06-14 12:50:15.387 MessageForward[67364:7174239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MFObject sendMessage]: unrecognized selector sent to instance 0x1006001a0'
*** First throw call 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 typeNSException
這個日志場景熟悉的不能再熟悉了,可以看出?_CF_forwarding_prep_0?函數(shù)調用了?___forwarding___?函數(shù),接著又調用了?doesNotRecognizeSelector?方法,最后拋出異常。但是靠這些是無法說服看客的,還得靠逆向工程反編譯后再反匯編成偽代碼來一探究竟,刨根問底。
__CF_forwarding_prep_0?和?___forwarding_prep_1___?函數(shù)都調用了?___forwarding___,只是傳入?yún)?shù)不同。___forwarding___有兩個參數(shù),第一個參數(shù)為將要被轉發(fā)消息的棧指針(可以簡單理解為IMP),第二個參數(shù)標記是否返回結構體。?__CF_forwarding_prep_0?第二個參數(shù)傳入?0,___forwarding_prep_1___?傳入的是 1,從函數(shù)名都能看得出來。下面是這兩個函數(shù)的偽代碼:
int __CF_forwarding_prep_0(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
? ? rax = ____forwarding___(rsp, 0x0);
? ? if (rax != 0x0) { // 轉發(fā)結果不為空,將內容返回
? ? ? ? ? ? rax = *rax;
? ? }
? ? else { // 轉發(fā)結果為空,調用 objc_msgSend(id self, SEL _cmd,...);
? ? ? ? ? ? rsi = *(rsp + 0x8);
? ? ? ? ? ? rdi = *rsp;
? ? ? ? ? ? rax = objc_msgSend(rdi, rsi);
? ? }
? ? return rax;
}
int ___forwarding_prep_1___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
? ? rax = ____forwarding___(rsp, 0x1);
? ? if (rax != 0x0) {// 轉發(fā)結果不為空,將內容返回
? ? ? ? ? ? rax = *rax;
? ? }
? ? else {// 轉發(fā)結果為空,調用 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);
? ? }
? ? return rax;
}
在?x86_64?架構中,rax?寄存器一般是作為返回值,rsp?寄存器是棧指針。在調用?objc_msgSend?函數(shù)時,參數(shù)?arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5?分別使用寄存器?rdi, rsi, rdx, rcx, r8, r9?的值。在調用?objc_msgSend_stret?時第一個參數(shù)為?st_addr,其余參數(shù)依次后移。為了能夠打包出?NSInvocation?實例并傳入后續(xù)的?forwardInvocation:?方法,在調用?___forwarding___?函數(shù)之前會先將所有參數(shù)壓入棧中。因為寄存器?rsp?為棧指針指向棧頂,所以?rsp?的內容就是self?啦,因為?x86_64?是小端,棧增長方向是由高地址到低地址,所以從棧頂往下移動一個指針需要加?0x8(64bit)。而將參數(shù)入棧的順序是從后往前的,也就是說?arg0?是最后一個入棧的,位于棧頂:
__CF_forwarding_prep_0:
0000000000085080 push rbp ; XREF=___CFInitialize+1380000000000085081 mov rbp, rsp
0000000000085084 sub rsp, 0xd0
000000000008508b mov qword [ss:rsp+0xb0], rax
0000000000085093 movq qword [ss:rsp+0xa0], xmm7
000000000008509c movq qword [ss:rsp+0x90], xmm6
00000000000850a5 movq qword [ss:rsp+0x80], xmm5
00000000000850ae movq qword [ss:rsp+0x70], xmm4
00000000000850b4 movq qword [ss:rsp+0x60], xmm3
00000000000850ba movq qword [ss:rsp+0x50], xmm2
00000000000850c0 movq qword [ss:rsp+0x40], xmm1
00000000000850c6 movq qword [ss:rsp+0x30], xmm0
00000000000850cc mov qword [ss:rsp+0x28], r9
00000000000850d1 mov qword [ss:rsp+0x20], r8
00000000000850d6 mov qword [ss:rsp+0x18], rcx
00000000000850db mov qword [ss:rsp+0x10], rdx
00000000000850e0 mov qword [ss:rsp+0x8], rsi
00000000000850e5 mov qword [ss:rsp], rdi
00000000000850e9 mov rdi, rsp ; argument #1 for method____forwarding___
00000000000850ec movrsi, 0x0; argument #2 for method____forwarding___
00000000000850f3 call____forwarding___
消息轉發(fā)的邏輯幾乎都卸載___forwarding___函數(shù)中了,實現(xiàn)比較復雜,反編譯出的偽代碼也不是很直觀。我對arigrant.com的結果完善如下:
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);
? // 調用 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);
? ?<breakPoint-interrupt>
? }
? // 調用 methodSignatureForSelector 獲取方法簽名后再調用 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);
}
這么一大坨代碼就是整個消息轉發(fā)路徑的邏輯,概況如下:?
1.先調用forwardingTargetForSelector方法獲取新的target作為receiver重新執(zhí)行selector,如果返回的內容不合法(為 nil 或舊receiver 一樣),那就進入第一步。?
2.調用?methodSignatureForSelector獲取方法簽名后,判斷返回類型信息是否正確,再調用forwardInvocation執(zhí)行?NSInvocation對象,并將結果返回。如果對象沒實現(xiàn)methodSignatureForSelector方法,進入第三步。?
3.調用?doesNotRecognizeSelector方法。
doesNotRecognizeSelector?之前其實還有個判斷selector 在Runtime 中是否注冊過的邏輯,但在我們正常發(fā)消息的時候,不會出現(xiàn)此問題。但如果手動創(chuàng)建一個?NSInvocation對象并調用?invoke,并將第二個參數(shù)設置成一個不存在的selector,那就會導致這個問題,并輸入日志”does not match selector known to Objective C runtime”。較真的讀者可能會有疑問:為何這段邏輯判斷用不到卻還存在著?難道除了?__CF_forwarding_prep_0?和?___forwarding_prep_1___函數(shù)還有其他函數(shù)調用___forwarding___么?莫非消息轉發(fā)還有其他路徑?其實并不是!原因是?___forwarding___調用了___forwarding___函數(shù),以下方法也會調用?___invoking___函數(shù):
-[NSInvocationinvoke]
-[NSInvocationinvokeUsingIMP:]
-[NSInvocationinvokeSuper]
doesNotRecognizeSelector?方法其實在libobj.A.dylib 中已經(jīng)廢棄了,而是在CF框架中實現(xiàn),而且也不是開源的。從函數(shù)調用棧可以發(fā)現(xiàn)?doesNotRecognizeSelector之后會拋出異常,而Runtime 中廢棄的實現(xiàn)只是打印日志后直接殺掉進程(__builtin_trap())。下面是CF中實現(xiàn)的偽代碼:
void -[NSObject doesNotRecognizeSelector:](void * self, void * _cmd, void * arg2) {
? ? r14 = ___CFFullMethodName([self class], 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 = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0];
? ? objc_exception_throw(rax);
? ? return;
}void +[NSObject doesNotRecognizeSelector:](void * self, void * _cmd, void * arg2) {
? ? r14 = ___CFFullMethodName([self class], 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 = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0];
? ? objc_exception_throw(rax);
? ? return;
}
也就是說我們可以override?doesNotRecognizeSelector?或者捕獲其爆出的異常。在這里還是大有文章可做的。
總結
我將整個流程繪制出來,過濾了一些不會進入的分支路徑和跟主題無關的細節(jié):
圖像地址:http://7ni3rk.com1.z0.glb.clouddn.com/MessageForward/消息發(fā)送與轉發(fā)路徑流程圖.jpg
介于國內關于這塊知識的好多文章描述不夠準確和詳細,或是對消息轉發(fā)的原理描述理解不夠深刻,或是側重貼源碼而欠思考,所以我做了一個比較全面詳細的講解。
Why objc_msgSend Must be Written in Assembly?