iOS 底層探索 - 消息轉發

在這裏插入圖片描述

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

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

我們先來到 _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);
        }
    }
}

大概的流程如下:

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

1.1 對象方法動態解析

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

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));
        }
    }
}

大致的流程如下:

  • 檢查是否實現了 +(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));
        }
    }
}

大致的流程如下:

  • 斷言是否是元類,如果不是,直接退出
  • 檢查是否實現了 +(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
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 爲什麼會多一步對象方法解析的流程了。

二、消息轉發快速流程

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

// 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 適用於消息轉發給其他能響應未知消息的對象,什麼意思呢,就是最終返回的內容必須和要查找的消息的參數和返回值一致,如果想要不一致,就需要走其他的流程。

三、消息轉發慢速流程

上面說到如果想要最終返回的內容必須和要查找的消息的參數和返回值不一致,需要走其他流程,那麼到底是什麼流程呢,我們接着看一下剛纔另外一個方法 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`,將無法識別的消息發送給對象將引發異常。

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

四、消息轉發流程圖

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

image.png

五、總結

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章