Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

派二星發表於2019-04-02

當訊息發沒有從子類和父類查詢到實現的時候,Runtime會給我們補救的機會。我們稱為訊息轉發機制。這個機制分為動態方法解析轉發

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

動態方法解析

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);//訊息轉發
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

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

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

原始碼裡lookForwardImp裡寫著 如果沒有實現該方法但是實現了resolve為true,且triedResolver為false則進入**_class_resolveMethod**,只會解析一次如果解析完成triedResolver就會變為true,進不來

/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
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,所以如果是類方法的話執行resolveClassMethod,那麼會一直查詢最後還會執行一次resolveinstancemethod,但是這個方法是執行的元類的resolveinstancemethod而不是類的,因為類是元類物件如果元類找不到就會往上層查詢,元類的上層是根元類,根元類的父類指向NSObject,所以最後還會執行一次resolveInstanceMethod最終的例項方法。**,如果是例項方法則resolveInstanceMethod方法進行解析,那麼我們在實際執行的時候會發現執行了兩次解析方法,因為__class_resolveInstanceMethod又幫我們傳送了一次訊息

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
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));
        }
    }
}

複製程式碼

然後可以在這個方法裡面進行resolveInstanceMethod返回true的時候動態增加方法。進行處理,如果不在這裡進行處理則訊息進入轉發流程。通過下面的isa走點陣圖也可以清晰的查詢出來。

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

訊息轉發流程

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

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

上面的動態方法解析如果沒有攔截則會進入__objc_msgForward_impcache這個方法,這個方法是由彙編進行呼叫的,沒有原始碼實現。原始碼實現是閉源的。大家都知道沒有攔截會進行快速轉發forwardTarget,然後快速轉發會通過methodSginatureForSelector進行方法簽名慢速轉發.,我們要分析的肯定不能這麼膚淺需要更深層次的來進行解析

extern void instrumentObjcMessageSends(Bool)


void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
複製程式碼

這個函式可以進行列印資訊比如列印一些底層log,這些資訊最終會儲存到一些地方地址是**./private/tmp** 目錄,可以通過這些函式檢視底層呼叫了那些資訊那麼怎麼使用這個函式呢。 定義一個Person類 定義一個walk方法,注意之前一定要寫上instrumentObjcMessageSends(YES)instrumentObjcMessageSends(NO)方法

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)

這裡就看到了訊息轉發的執行過程

initialize -> resolveClassMethod -> resolveInstanceMethod -> forwardingTargetForSelector -> methodSignatureForSelector -> doesNotRecognizeSelector

./private/tmp那麼這個路徑是怎麼來的我們可以看到instrumentObjcMessageSends的原始碼裡有一個objcMsgLogEnabled確認可以列印資訊 然後執行objcMsgLogFD == -1 這是一個靜態屬性 然後logMessageSend會執行 當objcMsgLogFD = -1的時候會輸出log當目錄。

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;
}
複製程式碼

那麼我們看到log輸入的流程當我們丟擲異常的時候系統是如何來成功丟擲來的呢,看到objc_msgForward_impcache的方法解析後執行了__objc_forward_handler

Runtime底層原理探究(三) --- 訊息轉發機制(動態方法解析)
然後_objc_forward_handler在檔案裡面是優雅的將崩潰資訊丟擲來了

 // 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;
複製程式碼

訊息轉發機制總結

OC的訊息傳送當接受這沒有響應的時候系統給了兩次反悔的機會,如果沒有響應則會進入 訊息轉發流程 首先是動態訊息解析通過resolveinstacnemessage,類方法是resolveclassmessage 進行處理如果返回true並通過runtime動態新增訊息進行處理。如果未進行處理進入複雜的轉發流程。這些流程是通過彙編進行的蘋果並未開源,那麼我們推斷可以通過instrumentObjcMessageSends進行處理的到對應路徑的一個文靜從哪裡可以看出響應的訊息載入流程,異常的丟擲是通過__objc_forward_handler,進行了對應的堆疊列印資訊

相關文章