消息發送和轉發流程能夠歸納爲:消息發送(Messaging)是 Runtime 經過 selector 快速查找 IMP 的過程,有了函數指針就能夠執行對應的方法實現;消息轉發(Message Forwarding)是在查找 IMP 失敗後執行一系列轉發流程的慢速通道,若是不做轉發處理,則會打日誌和拋出異常。 ###八面玲瓏的 objc_msgSend 此函數是消息發送必經之路,但只要一提 objc_msgSend,都會說它的僞代碼以下或相似的邏輯,反正就是獲取 IMP 並調用:c++
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。bash
其實在 objc-msg-x86_64.s 中包含了多個版本的 objc_msgSend 方法,它們是根據返回值的類型和調用者的類型分別處理的:架構
上一節中說到的 _class_lookupMethodAndLoadCache3 函數其實只是簡單的調用了 lookUpImpOrForward 函數:app
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:ide
1 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 添加操做就會被忽略掉。
1 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 的僞代碼描述得很傳神啊,由於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,因此若是不考慮返回值類型爲結構體的狀況,用那幾行僞代碼來表示複雜的彙編實現仍是挺恰當的。
_objc_msgForward_impcache 只是個內部的函數指針,只存儲於上節提到的類的方法緩存中,須要被轉化爲 _objc_msgForward 和 _objc_msgForward_stret 才能被外部調用。但在 Mac OS X macOS 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
複製代碼
稍微懂彙編的人一眼就看明白了,不懂的看註釋也懂了,我就不墨跡了。如今總算是把消息轉發前的邏輯繞回來構成閉環了。 上一節中提到 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。
在 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 函數,實現也是簡單粗暴,就是賦值啊:
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
複製代碼
重頭戲在於對 objc_setForwardHandler 的調用,以及以後的消息轉發調用棧。這回不是在 Objective-C Runtime (libobjc.dylib)中啦,而是在 Core Foundation(CoreFoundation.framework)中。雖然 CF 是開源的,但有意思的是蘋果故意在開源的代碼中刪除了在 CFRuntime.c 文件 __CFInitialize() 中調用 objc_setForwardHandler 的代碼。__CFInitialize() 函數是在 CF runtime 鏈接到進程時初始化調用的。從反編譯獲得的彙編代碼中能夠很容易跟 C 源碼對比出來,我用紅色標出了同一段代碼的差別。 彙編語言仍是比較好理解的,紅色標出的那三個指令就是把 __CF_forwarding_prep_0 和 forwarding_prep_1 做爲參數調用 objc_setForwardHandler 方法(那麼以前那兩個 DefaultHandler 卵用都沒有咯,反正不出意外會被 CF 替換掉): 反編譯後的 __CFInitialize() 彙編代碼 反編譯後的 __CFInitialize() 彙編代碼 然而在源碼中對應的代碼卻被刪掉啦: 蘋果提供的 __CFInitialize() 函數源碼 蘋果提供的 __CFInitialize() 函數源碼 在早期版本的 CF 源碼中,仍是能夠看到 __CF_forwarding_prep_0 和 forwarding_prep_1 的聲明的,可是不會有實現源碼,也沒有對 objc_setForwardHandler 的調用。這些細節從函數調用棧中沒法看出,只能逆向工程看彙編指令。但從函數調用棧能夠看出 __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 type NSException
複製代碼
這個日誌場景熟悉得不能再熟悉了,能夠看出 _CF_forwarding_prep_0 函數調用了 forwarding 函數,接着又調用了 doesNotRecognizeSelector 方法,最後拋出異常。可是靠這些是沒法說服看客的,還得靠逆向工程反編譯後再反彙編成僞代碼來一探究竟,刨根問底。 __CF_forwarding_prep_0 和 forwarding_prep_1 函數都調用了 forwarding,只是傳入參數不一樣。forwarding 有兩個參數,第一個參數爲將要被轉發消息的棧指針(能夠簡單理解成 IMP),第二個參數標記是否返回結構體。__CF_forwarding_prep_0 第二個參數傳入 0,forwarding_prep_1 傳入的是 1,從函數名都能看得出來。下面是這兩個函數的僞代碼:
int __CF_forwarding_prep_0(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
rax = ____forwarding___(rsp, 0x0);
if (rax != 0x0) { // 轉發結果不爲空,將內容返回
rax = *rax;
}
else { // 轉發結果爲空,調用 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) {// 轉發結果不爲空,將內容返回
rax = *rax;
}
else {// 轉發結果爲空,調用 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 函數時,參數 arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5 分別使用寄存器 rdi, rsi, rdx, rcx, r8, r9 的值。在調用 objc_msgSend_stret 時第一個參數爲 st_addr,其他參數依次後移。爲了可以打包出 NSInvocation 實例並傳入後續的 forwardInvocation: 方法,在調用 forwarding 函數以前會先將全部參數壓入棧中。由於寄存器 rsp 爲棧指針指向棧頂,因此 rsp 的內容就是 self 啦,由於 x86_64 是小端,棧增加方向是由高地址到低地址,因此從棧頂往下移動一個指針須要加 0x8(64bit)。而將參數入棧的順序是從後往前的,也就是說 arg0 是最後一個入棧的,位於棧頂:
__CF_forwarding_prep_0:
0000000000085080 push rbp ; XREF=___CFInitialize+138
0000000000085081 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 mov rsi, 0x0 ; argument #2 for method ____forwarding___
00000000000850f3 call ____forwarding___
複製代碼
消息轉發的邏輯幾乎都寫在 forwarding 函數中了,實現比較複雜,反編譯出的僞代碼也不是很直觀。我對 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 是否已經在 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);
}
複製代碼
這麼一大坨代碼就是整個消息轉發路徑的邏輯,歸納以下: 先調用 forwardingTargetForSelector 方法獲取新的 target 做爲 receiver 從新執行 selector,若是返回的內容不合法(爲 nil 或者跟舊 receiver 同樣),那就進入第二步。 調用 methodSignatureForSelector 獲取方法簽名後,判斷返回類型信息是否正確,再調用 forwardInvocation 執行 NSInvocation 對象,並將結果返回。若是對象沒實現 methodSignatureForSelector 方法,進入第三步。 調用 doesNotRecognizeSelector 方法。 doesNotRecognizeSelector 以前其實還有個判斷 selector 在 Runtime 中是否註冊過的邏輯,但在咱們正常發消息的時候不會出此問題。但若是手動建立一個 NSInvocation 對象並調用 invoke,並將第二個參數設置成一個不存在的 selector,那就會致使這個問題,並輸入日誌 「does not match selector known to Objective C runtime」。較真兒的讀者可能會有疑問:何這段邏輯判斷乾脆用不到卻還存在着?難道除了 CF_forwarding_prep_0 和 forwarding_prep_1 函數還有其餘函數也調用 forwarding 麼?莫非消息轉發還有其餘路徑?其實並非!緣由是 forwarding 調用了 invoking 函數,因此上面的僞代碼直接把 invoking 函數的邏輯也『翻譯』過來了。除了 forwarding 函數,如下方法也會調用___invoking_ 函數:
-[NSInvocation invoke]
-[NSInvocation invokeUsingIMP:]
-[NSInvocation invokeSuper]
複製代碼
doesNotRecognizeSelector 方法其實在 libobj.A.dylib 中已經廢棄了,而是在 CF 框架中實現,並且也不是開源的。從函數調用棧能夠發現 doesNotRecognizeSelector 以後會拋出異常,而 Runtime 中廢棄的實現只是打日誌後直接殺掉進程(__builtin_trap())。下面是 CF 中實現的僞代碼:
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 或者捕獲其拋出的異常。在這裏仍是大有文章可作的。
我將整個實現流程繪製出來,過濾了一些不會進入的分支路徑和跟主題無關的 yulingtianxia.com/resources/M…
細節: 消息發送與轉發路徑流程圖 消息發送與轉發路徑流程圖 介於國內關於這塊知識的好多文章描述不夠準確和詳細,或是對消息轉發的原理描述理解不夠深入,或是側重貼源碼而欠思考,因此我作了一個比較全面詳細的講解。