手把手帶你探索Runtime底層原理(二)動態方法解析和消息轉發

前言

繼續上篇Runtime底層原理(一)方法查找,在上篇說到若是沒有找到imp,就會結束方法查找,而後進入動態方法解析和消息轉發。編程

動態方法解析

// No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // 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;
    }
複製代碼
  • 這裏判斷了類有沒有解析和是否嘗試解析過的標記值triedResolver,再次解析後會把triedResolver設置爲YES,只解析一次。
  • 重點在_class_resolveMethod這個解析函數
/*********************************************************************** * _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);
        }
    }
}
複製代碼
  • 判斷了這個類是否是元類isMetaClass,若是是非元類則只調用_class_resolveInstanceMethod,若是是元類則二者均可能調用
    • 這裏有個疑問,爲何要這麼判斷?

_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*/);
複製代碼
  • assert(cls->isMetaClass());這裏一開始就有驗證這個類是否是元類
  • 這裏發送了一個SEL_resolveClassMethod給這個對象_class_getNonMetaClass(cls, inst),即給一個非元類對象(類),發送了一個類方法消息,點進去看這個對象生成的方法
Class _class_getNonMetaClass(Class cls, id obj)
{
    mutex_locker_t lock(runtimeLock);
    cls = getNonMetaClass(cls, obj);
    assert(cls->isRealized());
    return cls;
}
複製代碼

繼續找getNonMetaClassbash

static Class getNonMetaClass(Class metacls, id inst) {
    static int total, named, secondary, sharedcache;
    runtimeLock.assertLocked();

    realizeClass(metacls);

    total++;

    // return cls itself if it's already a non-meta class
    if (!metacls->isMetaClass()) return metacls;

    // metacls really is a metaclass

    // special case for root metaclass
    // where inst == inst->ISA() == metacls is possible
    if (metacls->ISA() == metacls) {
        Class cls = metacls->superclass;
        assert(cls->isRealized());
        assert(!cls->isMetaClass());
        assert(cls->ISA() == metacls);
        if (cls->ISA() == metacls) return cls;
    }

    // use inst if available
    if (inst) {
        Class cls = (Class)inst;
        realizeClass(cls);
        // cls may be a subclass - find the real class for metacls
        while (cls  &&  cls->ISA() != metacls) {
            cls = cls->superclass;
            realizeClass(cls);
        }
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
            return cls;
        }
        //省略其餘代碼
複製代碼
  1. if (!metacls->isMetaClass()) return metacls; 若是已是非元類,則返回class自己,不繼續走以後的邏輯
  2. if (metacls->ISA() == metacls){...}若是這個元類的isa指向本身,並且它的superclass的isa也指向本身,則表示這個類是根元類,帳直接返回
  • 這裏應該怎麼理解呢?咱們看到下圖,根元類的isa指針是指向本身的,它的superclassrootClass,而後它的isa指針也是指向這個根元類的root metaclass
    isa走位圖
  1. 若是不是根元類,則繼續找class的isa是指向元類的這個類cls->ISA(),並且這個類不能是子類
if (inst) {
        Class cls = (Class)inst;
        realizeClass(cls);
        // cls may be a subclass - find the real class for metacls
        while (cls  &&  cls->ISA() != metacls) {
            cls = cls->superclass;
            realizeClass(cls);
        }
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
            return cls;
        }
    }
複製代碼

_class_resolveInstanceMethod分析

回到剛纔那裏,再看到源碼_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);
    //省略下面無關代碼
}
複製代碼
  • 首先lookUpImpOrNil的內部是經過lookUpImpOrForward方法進行查找,再次回到遞歸調用,lookUpImpOrForward_class_resolveMethod是互相循環調用的。post

  • 若是仍是沒查到,這裏就不會再次進入動態方法解析(注:若是再次進入動態方法解析會造成死遞歸),首先對cls的元類進行查找,而後元類的父類,也就是根元類(系統默認實現的虛擬的)進行查找、最終到NSObject,只不過NSObject中默認實現resolveInstanceMethod方法返回NO,也就是此時在元類進行查找的時候找到了resolveInstanceMethod方法,並中止繼續查找,這就是爲何動態方法解析後的遞歸沒有再次進入動態方法解析的緣由。若是最終仍是沒有找到SEL_resolveInstanceMethod則說明程序有問題。ui

  • 再看到msg(cls, SEL_resolveInstanceMethod, sel);這裏發送了一個SEL_resolveInstanceMethod給這個對象cls,即給一個元類,發送了一個實例方法消息spa

  • 這裏該怎麼理解呢?咱們看到上面以前有一個結論:給一個非元類對象(類),發送了一個類方法消息。其實二者是同樣的,由於對象方法存在於類中,以實例方法存在,類方法存在於元類中,也以實例方法存在。3d

  • 以下面的源碼,獲取一個類的類方法,其實就等於獲取這個元類的實例方法,深入理解上面的isa走位圖指針

/*********************************************************************** * class_getClassMethod. Return the class method for the specified * class and selector. **********************************************************************/
Method class_getClassMethod(Class cls, SEL sel) {
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}
複製代碼

小結

非元類其實會沿着繼承鏈,去找這個類有沒有實現+resolveClassMethod。 若是沒有,還會進行下一步補救措施,從它的元類裏找有沒有實現_class_resolveInstanceMethod,元類中沒有找到就會去根元類中查找,一直查到NSObjct日誌

動態解析方法的使用及做用

.h實現2個方法:code

- (void)run;
+ (void)eat;
複製代碼

.m 沒有實現上面的兩個方法:

- (void)walk {
    NSLog(@"%s",__func__);
}
+ (void)drink {
    NSLog(@"%s",__func__);
}

// .m沒有實現,而且父類也沒有,那麼咱們就開啓動態方法解析
//- (void)walk{
// NSLog(@"%s",__func__);
//}
//+ (void)drink{
// NSLog(@"%s",__func__);
//}


#pragma mark - 動態方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        // 咱們動態解析咱們的 對象方法
        NSLog(@"對象方法解析走這裏");
        SEL walkSEL = @selector(walk);
        Method readM= class_getInstanceMethod(self, walkSEL);
        IMP readImp = method_getImplementation(readM);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    return [super resolveInstanceMethod:sel];
}


+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(eat)) {
        // 咱們動態解析咱們的 對象方法
        NSLog(@"類方法解析走這裏");
        SEL drinkSEL = @selector(drink);
        // 類方法就存在咱們的元類的方法列表
        // 類 類犯法
        // 元類 對象實例方法
        // Method hellowordM1= class_getClassMethod(self, hellowordSEL);
        Method drinkM= class_getInstanceMethod(object_getClass(self), drinkSEL);
        IMP drinkImp = method_getImplementation(drinkM);
        const char *type = method_getTypeEncoding(drinkM);
        NSLog(@"%s",type);
        return class_addMethod(object_getClass(self), sel, drinkImp, type);
    }
    return [super resolveClassMethod:sel];
}
複製代碼

上面的代碼就是利用動態方法解析的實例,那麼它的做用有哪些呢?

適用於重定向,也能夠作防崩潰處理,也能夠作一些錯誤日誌收集等等。動態方法解析本質就是提供機會(任何沒有實現的方法均可以從新實現)

消息轉發流程

在經歷過上面的動態方法解析後,若是尚未找到,就會進入蘋果還沒有開源的消息轉發流程。

// No implementation found, and method resolver didn't help. 
    // Use forwarding.
    //_objc_msgForward
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
複製代碼

找到_objc_msgForward_impcache,發現是一個未實現的方法,也跟不進源碼

#if !OBJC_OLD_DISPATCH_PROTOTYPES
extern void _objc_msgForward_impcache(void);
#else
extern id _objc_msgForward_impcache(id, SEL, ...);
#endif
複製代碼

很顯然,去以前的彙編文件objc-msg-arm64.s裏搜索,看到了彙編的實現,這裏提到了一個__objc_forward_handler回調處理函數

ENTRY __objc_msgForward

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

在彙編裏沒有找到它的實現,而後在源碼文件裏找到了它,能夠看到這個方法,打印的剛好就是咱們調用未實現函數的崩潰信息...unrecognized selector...

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

走到這裏,仍是沒有找到咱們想看的方法轉發流程,因爲蘋果這部分代碼是還沒有開源的,因此並不能看到源碼實現,可是能夠經過另外一種方式instrumentObjcMessageSends來驗證消息轉發流程。

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

而後再看objcMsgLogFD,能夠看到它會生成一個文件/tmp/msgSends-%d,存放日誌

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

這個函數能夠打印在objc消息過程當中的全部日誌,因此以下在調用run方法前開啓日誌打印

instrumentObjcMessageSends(YES);
        [[Person alloc] run];
        instrumentObjcMessageSends(NO);
複製代碼

運行後,果真發如今文件夾/private/tmp多了一個msgSends-xxx的文件,打開能夠看到裏面的調用過程

再對比消息轉發流程圖,日誌驗證了這整個調用過程

消息轉發流程的使用及做用

代碼示例(實例對象消息轉發):

#pragma mark - 實例對象消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    // if (aSelector == @selector(run)) {
    // // 轉發給Student對象
    // return [Student new];
    // }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // forwardingTargetForSelector 沒有實現,就只能方法簽名了
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"------%@-----",anInvocation);
    anInvocation.selector = @selector(walk);
    [anInvocation invoke];
}
複製代碼

它的應用場景和動態方法解析相似,在異常捕獲防崩潰,重定向,方法交換Method swizzling,切面編程的Aspects源碼等均可以使用它。

總結

Runtime就是C、C++、彙編實現的一套API,給OC增長的一個運行時功能,也就是咱們平時所說的運行時。 運行時: 在程序運行時,纔會去肯定對象的類型,並調用類與對象對應的方法。 Runtime的消息機制完美的提現了運行時的特徵。

以上均爲我的探索源碼的理解和所得,若有錯誤請指正,歡迎討論。

相關文章
相關標籤/搜索