1. 程式人生 > >Objective-C中的訊息傳送總結

Objective-C中的訊息傳送總結

關於OC中的訊息傳送的實現,在去年也看過一次,當時有點不太理解,但是今年再看卻很容易理解。
我想這跟知識體系的構建有關,如果你不認識有磚、水泥等這些建築的基本組成部分,那麼我們應該很難理解建築是怎麼建造出來的吧?
學習新知識,應該也是同樣的道理!

資料

今年再看 訊息傳送機制時,也翻了很多文章,本來想自己總結一遍的,但是感覺這篇 Objective-C 訊息傳送與轉發機制原理 實在寫的太好了,就直接轉載了。
原文:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

訊息傳送和轉發流程可以概括為:訊息傳送(Messaging)是Runtime通過selector 快速查詢IMP的過程,有了函式指標就可以執行對應的方法實現;訊息轉發(Message Frowarding)是在查詢IMP失敗後一系列轉發流程的慢速通道,如果不做轉發處理,則會打日誌和丟擲異常。

本文不講述開發者在訊息傳送和轉發流程中需要做的事,而是講述原理。能夠很好地閱讀本文的前提是你對 Objective-C Runtime 已經有一定的瞭解,關於什麼是訊息,Class的結構,Selector、IMP、元類等概念將不再贅述。本文用到的原始碼為objc-680 和 CF-1153.18,逆向CoreFoundation.framework的系統版本為macOS10.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使用匯編語言寫的,針對不同架構有不同的實現(我們可以在objc-680的Source目錄下看到多個objc-msg-xxxx的彙編實現檔案)。如下為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 向後移位bbyte),最後還要用epilogue打掃戰場。而且彙編程式執行效率高,在Objective-C Runtime中呼叫頻率較高的函式好多都用匯編編寫的。

objc_msgSend_fpret 後面fpret 其實是float point return 的縮寫;stret 就是struct return的縮寫,其他同理。
關於 Calling Convention,可以去看Bang 的文章動態呼叫C函式的 Calling Convention一節

使用 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接著做了如下兩件事:
1.如果使用快取(cache引數為 YES),那就呼叫 cache_getImp方法從快取查詢IMP。cache_getImp 是用匯編語言寫的,也可以在 objc-msg-x86_64.s中找到,其依然用了之前說過的 CacheLookup 巨集。因為 _class_lookupMethodAndLoadCache3 呼叫 lookUpImpOrForward 時,cache 引數為 NO,這步直接略過。
2.如果是第一次用到這個類且 initialize 引數為 YESinitialize && !cls->isInitialized()),需要進行初始化工作,也就是開闢一個用於讀寫資料的空間。先對 runtimeLock 寫操作加鎖,然後呼叫 clsinitialize 方法。如果 sel == initialize 也沒關係,雖然 initialize 還會被呼叫一次,但不會起作用啦,因為 cls->isInitialized() 已經是 YES 啦。

繼續在類的繼承體系中查詢

考慮到執行時類中的方法可能會增加,需要先做讀操作加鎖,使得方法查詢和快取填充成原子操作。新增category 會重新整理快取,之後如果舊資料又被重填到快取中,category 新增操作就會被忽略掉。

runtimeLock.read();

之後的邏輯整理如下:
1.如果 selector 是需要被忽略的垃圾回收用到的方法,則將 IMP 結果設為 _objc_ignored_method,這是個彙編程式入口,可以理解為一個標記。對此種情況進行快取填充操作後,跳到第 7 步;否則執行下一步。
2.查詢當前類中的快取,跟之前一樣,使用 cache_getImp 彙編程式入口。如果命中快取獲取到了 IMP,則直接跳到第 7 步;否則執行下一步。
3.在當前類中的方法列表(method list)中進行查詢,也就是根據 selector 查詢到 Method 後,獲取 Method 中的 IMP(也就是 method_imp 屬性),並填充到快取中。查詢過程比較複雜,會針對已經排序的列表使用二分法查詢,未排序的列表則是線性遍歷。如果成功查詢到 Method 物件,就直接跳到第 7 步;否則執行下一步。
4.在繼承層級中遞歸向父類中查詢,情況跟上一步類似,也是先查詢快取,快取沒中就查詢方法列表。這裡跟上一步不同的地方在於快取策略,有個 _objc_msgForward_impcache 彙編程式入口作為快取中訊息轉發的標記。也就是說如果在快取中找到了 IMP,但如果發現其內容是 _objc_msgForward_impcache,那就終止在類的繼承層級中遞迴查詢,進入下一步;否則跳到第 7 步。
5.當傳入lookUpImpOrForward的引數resolverYES並且是第一次進入第5步時,進入動態方法解析;否則進入下一步。這步是訊息轉發前的最後一次機會。此時釋放讀入鎖(runtimeLock.unlockRead()),接著間接地傳送+resolveInstanceMethod+resolveClassMethod訊息。這相當於告訴程式設計師『趕緊用 Runtime 給類裡這個 selector 弄個對應的 IMP 吧』,因為此時鎖已經unlock了所以不會快取結果,甚至還需要軟性地處理快取過期問題可能帶來的錯誤。這裡的業務邏輯稍微複雜些,後面會總結。因為這些工作都是在非執行緒安全下進行的,完成後需要回到第1步再次查詢IMP.
6.此時不僅沒查詢到IMP,動態方法解析也不奏效,只能將_objc_msgForward_impcache當做IMP並寫入快取。這也就是之前第4步中為何查詢到_objc_msgForward_impcache就表明了要進入訊息轉發了。
7.讀操作解鎖,並將之前找到的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,後面會講到它。lookUpImpOrNillookUpImpOrForward 的功能很相似,只是將 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才能被外部呼叫。但在 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

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 函式,實現也是簡單粗暴,就是賦值啊:

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()函式原始碼

在早起版本的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);
}

這麼一大坨程式碼就是整個訊息轉發路徑的邏輯,概況如下:
1.先呼叫forwardingTargetForSelector方法獲取新的target作為receiver重新執行selector,如果返回的內容不合法(為 nil 或舊receiver 一樣),那就進入第一步。
2.呼叫 methodSignatureForSelector獲取方法簽名後,判斷返回型別資訊是否正確,再呼叫forwardInvocation執行 NSInvocation物件,並將結果返回。如果物件沒實現methodSignatureForSelector方法,進入第三步。
3.呼叫 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___呼叫了___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 或者捕獲其爆出的異常。在這裡還是大有文章可做的。

總結

我將整個流程繪製出來,過濾了一些不會進入的分支路徑和跟主題無關的細節:

介於國內關於這塊知識的好多文章描述不夠準確和詳細,或是對訊息轉發的原理描述理解不夠深刻,或是側重貼原始碼而欠思考,所以我做了一個比較全面詳細的講解。

參考文獻

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