iOS探索 動態方法解析和訊息轉發機制

我是好寶寶發表於2020-02-12

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

上一篇文章講了方法在底層是如何通過sel找到imp的,本文就將通過原始碼來研究“沒有實現的方法在底層要通過多少關卡才能發出unrecognized selector sent to instanceCrash”,看完本文後你會明白程式崩潰也是一個很複雜的過程

動態方法決議原始碼中,FXSon中有兩個只宣告未實現的方法,分別呼叫它們:

  • - (void)doInstanceNoImplementation;
  • + (void)doClassNoImplementation;

一、訊息查詢流程

訊息查詢流程部分不再展開講解,未實現方法查詢主要經過以下流程:

  • 彙編中通過isa平移得到class,記憶體偏移得到cache->buckets查詢快取
  • c++中
    • 先查詢本類快取,再找本類方法列表
    • 遍歷父類:查詢父類快取,再找父類方法列表

iOS探索 動態方法解析和訊息轉發機制

由於慢速流程呼叫的是lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/),遍歷父類無果後來到動態方法解析

二、動態方法解析

只有resolvertriedResolver滿足條件下才會進入動態方法解析

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

動態方法解析按呼叫方法走不同分支:

  • cls是元類的話說明呼叫類方法,走_class_resolveInstanceMethod
  • 非元類的話呼叫了例項方法,走_class_resolveInstanceMethod
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);
        }
    }
}
複製程式碼

1.例項方法

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

①檢查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)方法

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
複製程式碼

注意這裡的lookUpImpOrForward中的resolver為NO,所以只會在本類和父類中查詢,並不會動態方法解析

但cls沒有這個方法,其實根類NSObject已經實現了這個方法(NSProxy沒有實現)

// 具體搜尋 NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
複製程式碼

②向本類傳送SEL_resolveInstanceMethod訊息,即呼叫這個方法

lookUpImpOrNil再次查詢當前例項方法imp,找到就填充快取,找不到就返回

④結束動態方法解析,回到lookUpImpOrForward方法將triedResolver置否並goto retry重新查詢快取和方法列表

2.例項方法流程圖

iOS探索 動態方法解析和訊息轉發機制

3.類方法

相較於例項方法,類方法就複雜多了

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

_class_resolveClassMethod進入

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

lookUpImpOrNil查詢SEL_resolveClassMethod(resolveClassMethod)是否實現

③向非元類傳送SEL_resolveClassMethod訊息(由於cls是元類,_class_getNonMetaClass(cls, inst)得到inst

lookUpImpOrNil再次查詢當前例項方法imp,找到就填充快取,找不到就返回

⑤結束_class_resolveClassMethodlookUpImpOrNil查詢selimp,若有imp則退出動態方法決議,若無則進入_class_resolveInstanceMethod

⑥檢查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)方法

⑦向本類傳送SEL_resolveInstanceMethod訊息

lookUpImpOrNil再次查詢當前例項方法imp,找到就填充快取,找不到就返回

⑨結束動態方法解析,回到lookUpImpOrForward方法將triedResolver置否並goto retry重新查詢快取和方法列表

4.類方法流程圖

iOS探索 動態方法解析和訊息轉發機制

5.動態方法決議

Objective-C提供了一種名為動態方法決議的手段,使得我們可以在執行時動態地為一個selector 提供實現,並在其中為指定的selector 提供實現即可——子類重寫+resolveInstanceMethod:+resolveClassMethod:

  • 對於例項方法

例項方法流程圖中可以看出,解決崩潰的方法就是resolveInstanceMethod階段新增一個備用實現

#import "FXSon.h"
#import <objc/message.h>

@implementation FXSon

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(doInstanceNoImplementation)) {
        NSLog(@"——————————找不到%@-%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
        IMP insteadIMP = class_getMethodImplementation(self, @selector(doInstead));
        Method insteadMethod = class_getInstanceMethod(self, @selector(doInstead));
        const char *instead = method_getTypeEncoding(insteadMethod);
        return class_addMethod(self, sel, insteadIMP, instead);
    }
    
    return NO;
}

- (void)doInstead {
    NSLog(@"——————————解決崩潰——————————");
}

@end
複製程式碼
  • 對於類方法——resolveClassMethod階段

效仿解決例項方法崩潰,類方法也可以往元類中塞一個imp例項方法存在類物件中,類方法存在元類物件中)

#import "FXSon.h"
#import <objc/message.h>

@implementation FXSon

+ (BOOL)resolveClassMethod:(SEL)sel {
    
    if (sel == @selector(doClassNoImplementation)) {
        NSLog(@"——————————找不到%@+%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
        IMP classIMP = class_getMethodImplementation(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
        Method classMethod = class_getInstanceMethod(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
        const char *cls = method_getTypeEncoding(classMethod);
        return class_addMethod(objc_getMetaClass("FXSon"), sel, classIMP, cls);
    }
    
    return NO;
}

+ (void)doClassNoInstead {
    NSLog(@"——————————解決崩潰——————————");
}

@end
複製程式碼
  • 對於類方法——resolveInstanceMethod階段

因為元類的方法以例項方法儲存在根元類中,由於元類根源類由系統建立無法修改,所以只能在根元類的父類NSObject中,重寫對應的例項方法resolveInstanceMethod進行動態解析(isa走點陣圖完美說明一切)

iOS探索 動態方法解析和訊息轉發機制

#import "NSObject+FX.h"
#import <objc/message.h>

@implementation NSObject (FX)

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if ([NSStringFromSelector(sel) isEqualToString:@"doClassNoImplementation"]) {
        NSLog(@"——————————找不到%@-%@方法,崩潰了——————————", self, NSStringFromSelector(sel));
        IMP instanceIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
        Method instanceMethod = class_getInstanceMethod(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
        const char *instance = method_getTypeEncoding(instanceMethod);
        return class_addMethod(objc_getMetaClass("NSObject"), sel, instanceIMP, instance);
    }

    return NO;
}

- (void)doInstanceNoInstead {
    NSLog(@"——————————解決崩潰——————————");
}

@end
複製程式碼

6.動態方法決議總結

  • 例項方法可以重寫resolveInstanceMethod新增imp
  • 類方法可以在本類重寫resolveClassMethod往元類新增imp,或者在NSObject分類重寫resolveInstanceMethod新增imp
  • 動態方法解析只要在任意一步lookUpImpOrNil查詢到imp就不會查詢下去——即本類做了動態方法決議,不會走到NSObjct分類的動態方法決議
  • 所有方法都可以通過在NSObject分類重寫resolveInstanceMethod新增imp解決崩潰

那麼把所有崩潰都在NSObjct分類中處理,加以字首區分業務邏輯,豈不是美滋滋?錯!

  • 統一處理起來耦合度高
  • 邏輯判斷多
  • 可能在NSObjct分類動態方法決議之前已經做了處理
  • SDK封裝的時候需要給一個容錯空間

這也不行,那也不行,那該怎麼辦?放心,蘋果爸爸已經給我們準備好後路了!

三、訊息轉發機制

lookUpImpOrForward方法在查詢類、父類快取和方法列表以及動態方法解析後,如果還沒有找到imp那麼將進入訊息處理的最後一步——訊息轉發流程

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

在彙編中發現了_objc_msgForward_impcache,如下是arm64的彙編程式碼

iOS探索 動態方法解析和訊息轉發機制

最後會來到c++中_objc_forward_handler

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

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

再來看看崩潰資訊,崩潰之前底層還呼叫了___forwarding____CF_forwarding_prep_0等方法,但是CoreFoundation庫不開源

iOS探索 動態方法解析和訊息轉發機制

在無從下手之際,只能根據前輩們的經驗開始著手——然後在logMessageSend找到了探索方向(lookUpImpOrForward->log_and_fill_cache->logMessageSend)

通過方法我們可以看到,日誌會記錄在/tmp/msgSends目錄下,並且通過objcMsgLogEnabled變數來控制是否儲存日誌

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

instrumentObjcMessageSends可以改變objcMsgLogEnabled的值

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

所以我們可以根據以下程式碼來記錄並檢視日誌(彷彿不能在原始碼工程中操作)

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXSon *son = [[FXSon alloc] init];
        
        instrumentObjcMessageSends(true);
        [son doInstanceNoImplementation];
        instrumentObjcMessageSends(false);
    }
}
複製程式碼

訪達shift+command+G訪問/tmp/msgSends

iOS探索 動態方法解析和訊息轉發機制
動態方法解析doesNotRecognizeSelector崩潰之間,就是訊息轉發流程——分為快速流程forwardingTargetForSelector慢速流程methodSignatureForSelector

1.快速流程

forwardingTargetForSelector在原始碼中只有一個宣告,並沒有其它描述,好在幫助文件中提到了關於它的解釋:

  • 該方法的返回物件是執行sel的新物件,也就是自己處理不了會將訊息轉發給別人的物件進行相關方法的處理,但是不能返回self,否則會一直找不到
  • 該方法的效率較高,如果不實現,會走到forwardInvocation:方法進行處理
  • 底層會呼叫objc_msgSend(forwardingTarget, sel, ...);來實現訊息的傳送
  • 被轉發訊息的接受者引數、返回值等應和原方法相同
    iOS探索 動態方法解析和訊息轉發機制

2.快速流程解決崩潰

如下程式碼就是是通過快速轉發解決崩潰——即FXSon實現不了的方法,轉發給FXTeacher去實現(轉發給已經實現該方法的物件)

#import "FXTeacher.h"

@implementation FXSon

// FXTeacher已實現了doInstanceNoImplementation
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(doInstanceNoImplementation)) {
        return [FXTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end
複製程式碼

3.慢速流程

在快速流程找不到轉發的物件後,會來到慢速流程methodSignatureForSelector

依葫蘆畫瓢,在幫助文件中找到methodSignatureForSelector

iOS探索 動態方法解析和訊息轉發機制

點選檢視forwardInvocation

  • forwardInvocationmethodSignatureForSelector必須是同時存在的,底層會通過方法簽名,生成一個NSInvocation,將其作為引數傳遞呼叫
  • 查詢可以響應NSInvocation中編碼的訊息的物件(對於所有訊息,此物件不必相同)
  • 使用anInvocation將訊息傳送到該物件。anInvocation將儲存結果,執行時系統將提取結果並將其傳遞給原始傳送者
    iOS探索 動態方法解析和訊息轉發機制

4.慢速流程解決崩潰

慢速流程流程就是先methodSignatureForSelector提供一個方法簽名,然後forwardInvocation通過對NSInvocation來實現訊息的轉發

#import "FXTeacher.h"

@implementation FXSon

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(doInstanceNoImplementation)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
   SEL aSelector = [anInvocation selector];

   if ([[FXTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[FXTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}

@end
複製程式碼

四、訊息轉發機制流程圖

iOS探索 動態方法解析和訊息轉發機制

寫在後面

有興趣的小夥伴們可以看看Demo,加深對OC訊息機制的理解和防崩潰的運用

相關文章