iOS 底層探索 - 消息轉發

iOS 底層探索系列緩存

iOS 查漏補缺系列markdown

1、動態方法解析流程分析

咱們在上一章《消息查找》分析到了動態方法解析,爲了更好的掌握具體的流程,咱們接下來直接進行源碼追蹤。app

咱們先來到 _class_resolveMethod 方法,該方法源碼以下:框架

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

大概的流程以下:ide

  • 判斷進行解析的是不是元類
  • 若是不是元類,則調用 _class_resolveInstanceMethod 進行對象方法動態解析
  • 若是是元類,則調用 _class_resolveClassMethod 進行類方法動態解析
  • 完成類方法動態解析後,再次查詢 cls 中的 imp,若是沒有找到,則進行一次對象方法動態解析

1.1 對象方法動態解析

咱們先分析對象方法的動態解析,咱們直接來到 _class_resolveInstanceMethod 方法處:oop

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製代碼

大體的流程以下:post

  • 檢查是否實現了 +(BOOL)resolveInstanceMethod:(SEL)sel 類方法,若是沒有實現則直接返回(經過 cls->ISA() 是拿到元類,由於類方法是存儲在元類上的對象方法)
  • 若是當前實現了 +(BOOL)resolveInstanceMethod:(SEL)sel 類方法,則經過 objc_msgSend 手動調用該類方法
  • 完成調用後,再次查詢 cls 中的 imp
  • 若是 imp 找到了,則輸出動態解析對象方法成功的日誌
  • 若是 imp 沒有找到,則輸出雖然實現了 +(BOOL)resolveInstanceMethod:(SEL)sel,而且返回了 YES,但並無查找到 imp 的日誌

image.png

1.2 類方法動態解析

接着咱們分析類方法動態解析,咱們直接來到 _class_resolveClassMethod 方法處:測試

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製代碼

大體的流程以下:ui

  • 斷言是不是元類,若是不是,直接退出
  • 檢查是否實現了 +(BOOL)resolveClassMethod:(SEL)sel 類方法,若是沒有實現則直接返回(經過 cls- 是由於當前 cls 就是元類,由於類方法是存儲在元類上的對象方法)
  • 若是當前實現了 +(BOOL)resolveClassMethod:(SEL)sel 類方法,則經過 objc_msgSend 手動調用該類方法,注意這裏和動態解析對象方法不一樣,這裏須要經過元類和對象來找到類,也就是 _class_getNonMetaClass
  • 完成調用後,再次查詢 cls 中的 imp
  • 若是 imp 找到了,則輸出動態解析對象方法成功的日誌
  • 若是 imp 沒有找到,則輸出雖然實現了 +(BOOL)resolveClassMethod:(SEL)sel,而且返回了 YES,但並無查找到 imp 的日誌

image.png

這裏有一個注意點,若是咱們把上面例子中的 objc_getMetaClass("LGPerson") 換成 self 試試,會致使 +(BOOL)resolveInstanceMethod:(SEL)sel 方法被調用,其實問題是發生在 class_getMethodImplementation 方法處,其內部會調用到 _class_resolveMethod 方法,而咱們的 cls 傳的是 self,因此又會走一次 +(BOOL)resolveInstanceMethod:(SEL)sel this

image.png

1.3 特殊的 NSObject 對象方法動態解析

咱們再聚焦到 _class_resolveMethod 方法上,若是 cls 是元類,也就是說進行的是類方法動態解析的話,有如下源碼:

_class_resolveClassMethod(cls, sel, inst); // 已經處理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 對象方法 決議
            _class_resolveInstanceMethod(cls, sel, inst);
        }
複製代碼

對於 _class_resolveClassMethod 的執行,確定是沒有問題的,只是爲何在判斷若是動態解析失敗以後,還要再進行一次對象方法解析呢,這個時候就須要上一張經典的 isa 走位圖了:

image.png

由這個流程圖咱們能夠知道,元類最終繼承於根元類,而根元類又繼承於 NSObject,那麼也就是說在根元類中存儲的類方法等價於在 NSObject 中存儲的對象方法。而系統在執行 lookUpImpOrNil 時,會遞歸查找元類的父類的方法列表。可是因爲元類和根元類都是系統自動生成的,咱們是沒法直接編寫它們,而對於 NSObject,咱們能夠藉助分類(Category)來實現統一的類方法動態解析,不過前提是類自己是沒有實現 resolveClassMethod 方法:

image.png

這也就解釋了爲何 _class_resolveClassMethod 爲何會多一步對象方法解析的流程了。

2、消息轉發快速流程

若是咱們沒有進行動態方法解析,消息查找流程接下來會來到的是什麼呢?

// No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
複製代碼

根據 lookUpImpOrForward 源碼咱們能夠看到當動態解析沒有成功後,會直接返回一個 _objc_msgForward_impcache。咱們嘗試搜索一下它,定位到 objc-msg-arm64.s 彙編源碼處:

STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
複製代碼

能夠看到在 __objc_msgForward_impcache 內部會跳轉到 __objc_msgForward,而 __objc_msgForward 內部咱們並拿不到有用的信息。這個時候是否是線索就斷了呢?咱們會議一下前面的流程,若是找到了 imp,會進行緩存的填充以及日誌的打印,咱們不妨找到打印的日誌文件看看裏面會不會有咱們須要的內容。

static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

bool logMessageSend(bool isClassMethod, const char *objectsClass, const char *implementingClass, SEL selector) {
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
複製代碼

這裏咱們很清楚的能看到日誌文件的存儲位置已經命名方式:

image.png

這裏還有一個注意點:

image.png

只有當 objcMsgLogEnabled 這個值爲 true 的時候纔會進行日誌的輸出,咱們直接搜索這個值出現過的地方:

image.png

image.png

很明顯,經過調用 instrumentObjcMessageSends 能夠來實現打印的開與關。咱們能夠簡單測試一下:

image.png

咱們運行一下,而後來到 /private/tmp 目錄下:

image.png

咱們打開這個文件:

image.png

咱們看到了熟悉的 resolveInstanceMethod,可是在這以後有 2 個以前咱們沒探索過的方法: forwardingTargetForSelectormethodSignatureForSelector。而後會有 doesNotRecognizeSelector 方法的打印,此時 Xcode 控制檯打印以下:

image.png

咱們能夠看到 ___forwarding___ 發生在 CoreFoundation 框架裏面。咱們仍是老規矩,以官方文檔爲準,查詢一下 forwardingTargetForSelectormethodSignatureForSelector

先是 forwardingTargetForSelector:

image.png

forwardingTargetForSelector 的官方定義是返回未找到 IMP 的消息首先定向到的對象,說人話就是在這個方法能夠實現狸貓換太子,不是找不到 IMP 嗎,我把這個消息交給其餘的對象來處理不就完事了嗎?咱們直接用代碼說話:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

這裏咱們直接返回 [LGTeacher alloc],咱們運行試試看:

image.png

完美~,咱們對 LGStudent 實例對象發送 saySomething 消息,結果最後是由 LGTeacher 響應了這個消息。關於 forwardingTargetForSelector ,蘋果還給出了幾點提示:

If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.) 譯: 若是一個對象實現或繼承了該方法,而後返回一個非空(非 self)的結果,那麼這個返回值會被當作新的消息接受者對象,消息會被轉發到該對象身上。(若是你在這個方法裏返回 self,那麼顯然就會發生一個死循環)。

If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking super’s implementation. 譯: 若是你在一個非基類中實現了該方法,而且這個類沒有任何能夠返回的內容,那麼你須要返回父類的實現。也就是 return [super forwardingTargetForSelector:aSelector];

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding. 譯: 這個方法使對象有機會在更昂貴的 forwardInvocation: 機械接管以前重定向發送給它的未知消息。當你只想將消息重定向到另外一個對象,而且比常規轉發快一個數量級時,這個方法就頗有用。在轉發的目標是捕獲 NSInvocation 或在轉發過程當中操縱參數或返回值的狀況下,此功能就無用了。

經過上面的官方文檔定義,咱們能夠理清下思路:

  • forwardingTargetForSelector 是一種快速的消息轉發流程,它直接讓其餘對象來響應未知的消息。
  • forwardingTargetForSelector 不能返回 self,不然會陷入死循環,由於返回 self 又回去當前實例對象身上走一遍消息查找流程,顯然又會來到 forwardingTargetForSelector
  • forwardingTargetForSelector 適用於消息轉發給其餘能響應未知消息的對象,什麼意思呢,就是最終返回的內容必須和要查找的消息的參數和返回值一致,若是想要不一致,就須要走其餘的流程。

3、消息轉發慢速流程

上面說到若是想要最終返回的內容必須和要查找的消息的參數和返回值不一致,須要走其餘流程,那麼究竟是什麼流程呢,咱們接着看一下剛纔另一個方法 methodSignatureForSelector 的官方文檔:

image.png

官方的定義是 methodSignatureForSelector 返回一個 NSMethodSignature 方法簽名對象,這個該對象包含由給定選擇器標識的方法的描述。

This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature. 譯: 這個方法用於協議的實現。同時在消息轉發的時候,在必須建立 NSInvocation 對象的狀況下,也會用到這個方法。若是您的對象維護一個委託或可以處理它不直接實現的消息,則應重寫此方法以返回適當的方法簽名。

咱們在文檔的最後能夠看到有一個叫 forwardInvocation: 的方法

image.png

咱們來到該方法的文檔處:

image.png

To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one. 譯:要響應對象自己沒法識別的方法,除了 forwardInvocation:外,還必須重寫methodSignatureForSelector: 。 轉發消息的機制使用從methodSignatureForSelector:得到的信息來建立要轉發的 NSInvocation 對象。 你的重寫方法必須爲給定的選擇器提供適當的方法簽名,方法是預先制定一個公式,也能夠要求另外一個對象提供一個方法簽名。

顯然,methodSignatureForSelectorforwardInvocation 不是孤立存在的,須要一塊兒出現。咱們直接上代碼說話:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}


- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);

   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}
複製代碼

而後查看打印結果:

image.png

能夠看到,先是來到了 methodSignatureForSelector,而後來到了 forwardInvocation,最後 saySomething 消息被查找到了。

關於 forwardInvocation,還有幾個注意點:

  • forwardInvocation 方法有兩個任務:
    • 查找能夠響應 inInvocation 中編碼的消息的對象。對於全部消息,此對象沒必要相同。
    • 使用 anInvocation 將消息發送到該對象。anInvocation 將保存結果,運行時系統將提取結果並將其傳遞給原始發送者。
  • forwardInvocation 方法的實現不只僅能夠轉發消息。forwardInvocation還能夠,例如,能夠用於合併響應各類不一樣消息的代碼,從而避免了必須爲每一個選擇器編寫單獨方法的麻煩。forwardInvocation 方法在對給定消息的響應中還可能涉及其餘幾個對象,而不是僅將其轉發給一個對象。
  • NSObjectforwardInvocation 實現:只會調用 dosNotRecognizeSelector:方法,它不會轉發任何消息。所以,若是選擇不實現forwardInvocation`,將沒法識別的消息發送給對象將引起異常。

至此,消息轉發的慢速流程咱們就探索完了。

4、消息轉發流程圖

咱們從動態消息解析到快速轉發流程再到慢速轉發流程能夠總結出以下的流程圖:

image.png

5、總結

咱們從 objc_msgSend 開始,探索了消息發送以後是怎麼樣的一個流程,這對於咱們理解 iOS 底層有很大的幫助。固然,限於筆者的水平,探索的過程可能會有必定的瑕疵。咱們簡單總結一下:

  • 動態方法解析分爲對象方法動態解析類方法動態解析
    • 對象方法動態解析須要消息發送者實現 +(BOOL)resolveInstanceMethod:(SEL)sel 方法
    • 類方法動態解析須要消息發送者實現 +(BOOL)resolveClassMethod:(SEL)sel 方法
  • 動態方法解析失敗會進入消息轉發流程
  • 消息轉發分爲兩個流程:快速轉發和慢速轉發
  • 快速轉發的實現是 forwardingTargetForSelector,讓其餘能響應要查找消息的對象來幹活
  • 慢速轉發的實現是 methodSignatureForSelectorforwardInvocation 的結合,提供了更細粒度的控制,先返回方法簽名給 Runtime,而後讓 anInvocation 來把消息發送給提供的對象,最後由 Runtime 提取結果真後傳遞給原始的消息發送者。

iOS 底層探索已經來到了第七篇,咱們接下來將會從 app 加載開始探索,探究 冷啓動熱啓動,以及 dyld 是如何工做的。

相關文章
相關標籤/搜索