1. 程式人生 > IOS開發 >手把手帶你探索Runtime底層原理(二)動態方法解析和訊息轉發

手把手帶你探索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,inst); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] _class_resolveClassMethod(cls,inst); if
(!lookUpImpOrNil(cls,inst,NO/*initialize*/,YES/*cache*/,NO/*resolver*/)) { _class_resolveInstanceMethod(cls,inst); } } } 複製程式碼
  • 判斷了這個類是不是元類isMetaClass,如果是非元類則只調用_class_resolveInstanceMethod,如果是元類則兩者都可能呼叫
    • 這裡有個疑問,為什麼要這麼判斷?

_class_resolveClassMethod分析

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

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

    BOOL (*msg)(Class,SEL,SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls,inst),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,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;
}
複製程式碼

繼續找getNonMetaClass

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,id inst)
{
    //判斷的目的是什麼?
    if (! lookUpImpOrNil(cls->ISA(),SEL_resolveInstanceMethod,cls,NO/*initialize*/,YES/*cache*/,NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class,SEL) = (typeof(msg))objc_msgSend;
    //呼叫的目的是為了什麼?
    bool resolved = msg(cls,sel);
    //省略下面無關程式碼
}
複製程式碼
  • 首先lookUpImpOrNil的內部是通過lookUpImpOrForward方法進行查詢,再次回到遞迴呼叫,lookUpImpOrForward_class_resolveMethod是互相迴圈呼叫的。

  • 如果還是沒查到,這裡就不會再次進入動態方法解析(注:如果再次進入動態方法解析會形成死遞迴),首先對cls的元類進行查詢,然後元類的父類,也就是根元類(系統預設實現的虛擬的)進行查詢、最終到NSObject,只不過NSObject中預設實現resolveInstanceMethod方法返回NO,也就是此時在元類進行查詢的時候找到了resolveInstanceMethod方法,並停止繼續查詢,這就是為什麼動態方法解析後的遞迴沒有再次進入動態方法解析的原因。如果最終還是沒有找到SEL_resolveInstanceMethod則說明程式有問題。

  • 再看到msg(cls,sel);這裡傳送了一個SEL_resolveInstanceMethod給這個物件cls,即給一個元類,傳送了一個例項方法訊息

  • 這裡該怎麼理解呢?我們看到上面之前有一個結論:給一個非元類物件(類),傳送了一個類方法訊息。其實兩者是一樣的,因為物件方法存在於類中,以例項方法存在,類方法存在於元類中,也以例項方法存在。

  • 如下面的原始碼,獲取一個類的類方法,其實就等於獲取這個元類的例項方法,深刻理解上面的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個方法:

- (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,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),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,imp,inst);
複製程式碼

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

#if !OBJC_OLD_DISPATCH_PROTOTYPES
extern void _objc_msgForward_impcache(void);
#else
extern id _objc_msgForward_impcache(id,...);
#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的訊息機制完美的提現了執行時的特徵。

以上均為個人探索原始碼的理解和所得,如有錯誤請指正,歡迎討論。