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

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

寫在前面

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

動態方法決議源碼中,FXSon中有兩個只聲明未實現的方法,分別調用它們:github

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

1、消息查找流程

消息查找流程部分再也不展開講解,未實現方法查找主要通過如下流程:緩存

  • 彙編中經過isa平移獲得class,內存偏移獲得cache->buckets查找緩存
  • c++中
    • 先查找本類緩存,再找本類方法列表
    • 遍歷父類:查找父類緩存,再找父類方法列表

因爲慢速流程調用的是lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/),遍歷父類無果後來到動態方法解析bash

2、動態方法解析

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

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

動態方法解析按調用方法走不一樣分支:ui

  • 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,因此只會在本類和父類中查找,並不會動態方法解析spa

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

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

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

②向本類發送SEL_resolveInstanceMethod消息,即調用這個方法

lookUpImpOrNil再次查找當前實例方法imp,找到就填充緩存,找不到就返回

④結束動態方法解析,回到lookUpImpOrForward方法將triedResolver置否並goto retry從新查找緩存和方法列表

2.實例方法流程圖

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.類方法流程圖

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走位圖完美說明一切)

#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封裝的時候須要給一個容錯空間

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

3、消息轉發機制

lookUpImpOrForward方法在查找類、父類緩存和方法列表以及動態方法解析後,若是尚未找到imp那麼將進入消息處理的最後一步——消息轉發流程

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

在彙編中發現了_objc_msgForward_impcache,以下是arm64的彙編代碼

最後會來到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庫不開源

在無從下手之際,只能根據前輩們的經驗開始着手——而後在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

動態方法解析doesNotRecognizeSelector崩潰之間,就是 消息轉發流程——分爲 快速流程forwardingTargetForSelector慢速流程methodSignatureForSelector

1.快速流程

forwardingTargetForSelector在源碼中只有一個聲明,並無其它描述,好在幫助文檔中提到了關於它的解釋:

  • 該方法的返回對象是執行sel的新對象,也就是本身處理不了會將消息轉發給別人的對象進行相關方法的處理,可是不能返回self,不然會一直找不到
  • 該方法的效率較高,若是不實現,會走到forwardInvocation:方法進行處理
  • 底層會調用objc_msgSend(forwardingTarget, sel, ...);來實現消息的發送
  • 被轉發消息的接受者參數、返回值等應和原方法相同

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

點擊查看forwardInvocation

  • forwardInvocationmethodSignatureForSelector必須是同時存在的,底層會經過方法簽名,生成一個NSInvocation,將其做爲參數傳遞調用
  • 查找能夠響應NSInvocation中編碼的消息的對象(對於全部消息,此對象沒必要相同)
  • 使用anInvocation將消息發送到該對象。anInvocation將保存結果,運行時系統將提取結果並將其傳遞給原始發送者

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

4、消息轉發機制流程圖

寫在後面

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

相關文章
相關標籤/搜索