iOS 底層探索:objc_msgSend 動態(tài)方法決議 & 消息轉(zhuǎn)發(fā)

iOS 底層探索: 學(xué)習(xí)大綱 OC篇

前言

  • OC調(diào)用方法,底層是調(diào)用 objc_msgSend 發(fā)送消息。在發(fā)送消息時(shí)會經(jīng)過一系列的快速 查找、慢速查找,如果查找到對應(yīng)的 IMP,直接返回;如果沒有找到,就會進(jìn)入到方法的動態(tài)方法決議和消息轉(zhuǎn)發(fā)流程。

  • 這篇文章就是深入探索動態(tài)方法決議消息轉(zhuǎn)發(fā)

準(zhǔn)備工作

一 、 動態(tài)方法決議

接著上一篇,在慢速查找流程未找到方法實(shí)現(xiàn)時(shí),首先會嘗試一次動態(tài)方法決議:

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //動態(tài)方法決議的控制條件
        behavior ^= LOOKUP_RESOLVER; 
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

進(jìn)入動態(tài)方法決議階段,源碼如下

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    runtimeLock.unlock();
//判斷是否是元類
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);//類執(zhí)行解析方式
    } 
    else {
        resolveClassMethod(inst, sel, cls); //元類執(zhí)行解析方式
// -- 如果resolveClassMethod找到了,就不會走這里了
//-- 如果沒找到,這個(gè)if必然會走,之前調(diào)用lookUpImpOrForward,已經(jīng)給該sel的方法緩存了imp = forward_imp
// -- 必然會走到done_nolock,返回一個(gè)nil
// -- 類方法在元類中也是以實(shí)例方法的形式存在,所以還需再走一遍實(shí)例方法的動態(tài)方法決議流程
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);//類執(zhí)行解析方式
        }
    }
//重新進(jìn)入 lookUpImpOrForward  根據(jù) behavior | LOOKUP_CACHE進(jìn)行判斷執(zhí)行方式
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

在resolveInstanceMethod方法中對實(shí)例方法動態(tài)解析,源碼如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 1\. 判斷系統(tǒng)是否實(shí)現(xiàn)SEL_resolveInstanceMethod方法
    // 即+(BOOL)resolveInstanceMethod:(SEL)sel, 
    // 繼承自NSObject的類,默認(rèn)實(shí)現(xiàn),返回NO
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) 
    {
        // Resolver not implemented.
        // 不是NSObject的子類,也未實(shí)現(xiàn)+(BOOL)resolveInstanceMethod:(SEL)sel,
        // 直接返回,沒有動態(tài)解析的必要
        return;
    }

    // 2\. 系統(tǒng)給你一次機(jī)會 - 你要不要針對 sel 來操作一下下
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);
    // 3\.-- 再去查一次imp,如果在cls的繼承鏈上自定義實(shí)現(xiàn)了`resolveInstanceMethod`方法并在里面添加了imp,就可以找到imp
    IMP imp = lookUpImpOrNil(inst, sel, cls);

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

在resolveClassMethod方法中對實(shí)例方法動態(tài)解析,源碼如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
//: -- 容錯(cuò)處理,判斷該元類的繼承鏈中是否有resolveClassMethod方法
//: -- 如果自定義沒實(shí)現(xiàn),則會找到NSObject
    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
//: -- 得到元類/類的對應(yīng)的類
//: -- 因?yàn)槲覀冎荒茉陬惱飳?shí)現(xiàn)resolveClassMethod方法,無法去元類實(shí)現(xiàn),所以這里把消息接受者設(shè)置為當(dāng)前類
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
//: -- 發(fā)送消息,調(diào)用nonmeta中的`resolveClassMethod `方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(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,如果上面調(diào)用用nonmeta中的`resolveClassMethod `方法里面給元類添加了imp,就會直接找到
    IMP imp = lookUpImpOrNil(inst, sel, cls);
//: --異常情況,打印錯(cuò)誤信息
    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));
        }
    }
}
  • 在經(jīng)過resolveMethod_locked方法后,進(jìn)行resolveInstanceMethod,重新進(jìn)行一遍lookUpImpOrForward;即: 如果動態(tài)方法決議中,將其實(shí)現(xiàn)指向了其他方法,則繼續(xù)查找指定的imp,即繼續(xù)慢速查找lookUpImpOrForward流程

  • 使用動態(tài)方法決議流程:
    resolveMethod_locked的流程圖
  • 使用動態(tài)方法決議,代碼舉例:

//對象使用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了",NSStringFromSelector(sel));
  /**
         添加方法

         @param self 調(diào)用該方法的對象
         @param sel 方法的名
         @param IMP 新添加的方法,是c語言實(shí)現(xiàn)的
         @param type :方法簽名, 新添加的方法的類型,包含函數(shù)的返回值以及參數(shù)內(nèi)容類型,eg:void xxx(NSString *name, int size),類型為:v@i
         */
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

//類使用
+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@ 來了",NSStringFromSelector(sel));
    if (sel == @selector(sayNB)) {

        IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

就算沒有實(shí)現(xiàn)方法say666 ,但是class_addMethod,依然不會崩潰。

優(yōu)化:根據(jù)resolveMethod_locked的流程圖,如果是元類調(diào)用,最后還是會走實(shí)例方法,根據(jù)isa走位圖,可知元類繼承鏈,最終繼承NSObject,所以我們可以給NSObject 寫個(gè)分類,直接在分類中綜合兩個(gè)方法如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

風(fēng)險(xiǎn):使用這方法的風(fēng)險(xiǎn)可能造成其他SDK 或者系統(tǒng)方法也使用了類似的方法,造成沖突。所以我們使用的類名LGPerson一定要特殊唯一。

注 :
使用這種辦法的前提是:相關(guān)方法代碼已經(jīng)實(shí)現(xiàn),只是在運(yùn)行時(shí)將改方法動態(tài)添加到目標(biāo)類中。。

二、 消息轉(zhuǎn)發(fā)機(jī)制

在方法查找過程中,經(jīng)過緩存查找,方法列表查找和動態(tài)方法解析,如果經(jīng)歷慢速查找都沒有查找到IMP,也沒有進(jìn)行方法動態(tài)解析,那么我們還有辦法進(jìn)行方法實(shí)現(xiàn): 消息轉(zhuǎn)發(fā)機(jī)制。但是我們只知道有消息轉(zhuǎn)發(fā)這個(gè)機(jī)制,但是我們始終找不到 消息轉(zhuǎn)發(fā)的實(shí)現(xiàn)方法和實(shí)現(xiàn)。這個(gè)時(shí)候:

開啟凡人的視角:

  • lldb方式斷點(diǎn)查看: 通過進(jìn)入 [person sayHello]之后的斷點(diǎn)查看在 class 為 LGPerson的時(shí)候 Sel ,可以發(fā)現(xiàn)forwardingTargetForSelectormethodSignatureForSelector。如圖:
image.png

開啟上帝視角:

  • 方式1:通過instrumentObjcMessageSends方式打印發(fā)送消息的日志

  • 方式2:通過hopper/IDA反編譯

方式1 :instrumentObjcMessageSends

通過lookUpImpOrForward --> log_and_fill_cache--> logMessageSend,在logMessageSend源碼下方找到instrumentObjcMessageSends的源碼實(shí)現(xiàn)如下:

/***********************************************************************
* instrumentObjcMessageSends
**********************************************************************/
// Define this everywhere even if it isn't used to simplify fork() safety code.
spinlock_t objcMsgLogLock;
#if !SUPPORT_MESSAGE_LOGGING

///開啟instrumentObjcMessageSends
void    instrumentObjcMessageSends(BOOL flag)
{
}
#else

///將方法執(zhí)行記錄通過打印出來
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
 .........
}
///結(jié)束instrumentObjcMessageSends
void instrumentObjcMessageSends(BOOL flag)
{
 .........
// SUPPORT_MESSAGE_LOGGING
#endif

所以,在main中調(diào)用instrumentObjcMessageSends打印方法調(diào)用的日志信息猶如上面的格式:

  • 1、在main中通過extern 聲明instrumentObjcMessageSends方法
  • 2、打開 objcMsgLogEnabled 開關(guān),即調(diào)用instrumentObjcMessageSends方法時(shí),傳入YES

不帶源碼的工程中這樣實(shí)現(xiàn):

///extern 修飾 我們可以拿來調(diào)用 蘋果提供了打印方法流程的方法 
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
//開啟
        instrumentObjcMessageSends(YES);
//去logMessageSend找打印信息
        [person sayHello];
//關(guān)閉
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

我們在objc4源碼工程再去找logMessageSend方法:

/***********************************************************************
* logMessageSend
**********************************************************************/
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

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
    ........
    return false;
}

運(yùn)行不帶源碼的工程,并前往/tmp/msgSends 目錄,發(fā)現(xiàn)有msgSends開頭的日志文件;
快捷鍵:command + shift + g ---> 輸入 /tmp/msgSends -->回車
打開msgSends.文件:

可以看出 ,崩潰前執(zhí)行了
兩次動態(tài)方法決議:resolveInstanceMethod方法
兩次消息快速轉(zhuǎn)發(fā):forwardingTargetForSelector方法
兩次消息慢速轉(zhuǎn)發(fā):methodSignatureForSelector + resolveInstanceMethod

方式2:通過hopper/IDA反編譯找到forwardingTargetForSelector等轉(zhuǎn)發(fā)方法的實(shí)現(xiàn)

  • Hopper和IDA是一個(gè)可以幫助我們靜態(tài)分析可視性文件的工具,可以將可執(zhí)行文件反匯編成偽代碼、控制流程圖等,下面以mac端Hopper為例(Hopper :要收費(fèi)的,IDA:是Windows端的)。

  • 通過打印崩潰的堆棧信息進(jìn)行查看崩潰信息如下:

通過路徑/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 獲取到CoreFoundation的可執(zhí)行文件:

CoreFoundation
  • 通過 hopper 進(jìn)行反編譯,步驟如下:
    打開hopper,選擇Try the Demo,然后將上一步的可執(zhí)行文件拖入hopper進(jìn)行反匯編,選擇x86(64 bits)如圖:

next:先搜索__forwarding_prep_0___

image.png

跳轉(zhuǎn)至:___forwarding___

  • 終于找到了forwardingTargetForSelector:方法了:咋們再來看看 其執(zhí)行流程,簡單分析如下:
    ___forwarding___簡單流程

開啟上帝視角之后,我們通過匯編分析,可以清晰看到轉(zhuǎn)發(fā)流程方法:

【快速轉(zhuǎn)發(fā)】forwardingTargetForSelector
【慢速轉(zhuǎn)發(fā)】methodSignatureForSelector —>forwardInvocation

注:forwardInvocation :消息重定向,以后再分析。

動態(tài)方法決議和消息轉(zhuǎn)發(fā)整體的流程如下:

注: 不論是否進(jìn)行動態(tài)決議都會重新lookUpImpOrForward,這里會有behavior的判斷,是否經(jīng)歷過動態(tài)決議處理。所以開始有一次動態(tài)決議處理方法的機(jī)會。

三、 實(shí)際舉例驗(yàn)證:

快速轉(zhuǎn)發(fā)實(shí)現(xiàn)
慢速轉(zhuǎn)發(fā)實(shí)現(xiàn)

注:如果不進(jìn)行快速消息轉(zhuǎn)發(fā) 也不進(jìn)行慢速轉(zhuǎn)發(fā)就會崩潰,可以試試。

四、 總結(jié)

    1. 消息轉(zhuǎn)發(fā)機(jī)制是在匯編中實(shí)現(xiàn)的,并且屬于CoreFoundation框架中,不開源的。我們可以通過反匯編的方式去查看;
    1. 在我們沒有實(shí)現(xiàn)方法的時(shí)候,慢速查找也找不到方法的時(shí)候,我們有三次機(jī)會去實(shí)現(xiàn)方法:
    • 1). 動態(tài)方法決議:實(shí)現(xiàn)resolveInstanceMethod
    • 2). 快速消息轉(zhuǎn)發(fā):實(shí)現(xiàn)forwardingTargetForSelector
    • 3). 慢速消息轉(zhuǎn)發(fā):實(shí)現(xiàn)methodSignatureForSelector —>forwardInvocation
    1. 如果objc_msgSend快速查找和慢速查找失敗,未實(shí)現(xiàn)動態(tài)方法決議和消息轉(zhuǎn)發(fā),則程序直接報(bào)錯(cuò)崩潰unrecognized selector sent to instance

五、 拓展

    1. objc_msgForward_impcache 的轉(zhuǎn)換 ;

objc_msgForward_impcache調(diào)用

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
......

_objc_msgForward_impcache源碼:

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 只是個(gè)內(nèi)部的函數(shù)指針,只存儲于類的方法緩存中,需要被轉(zhuǎn)化為_objc_msgForward 才能被外部調(diào)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。