iOS原理探索09--objc_msgSend流程分析之 动态方法决议 & 消息转发

在前面的两篇博客iOS原理探索08--objc_msgSend慢速查找流程分析iOS原理探索07--objc_msgSend快速查找流程分析中,我们知道方法的调用查找流程:先在缓存中进行快速查找,如果快速查找没有找到,那么会进入慢速查找流,在方法的列表中进行查找,在这慢速查找流程结束后没有找到的时候,会执行一次动态决议方法,如果动态决议没有找到,会进行消息转发。如果消息转发也没有那么就会来到我们平时开发中的unrecognized selector sent to instance报错提示!

日常开发中没有方法实现的报错分析

  • 条件设置:创建一个LGPerson类,添加一个+(void)sayNB方法,但是并没有实现该方法;在main函数中通过初始化一个LGPerson类对象,调用sayNB方法,运行程序。
运行结果

可以看到,这里已经报错了。下面可以跟进源码来看一下报错的源代码实现。

  • 源代码实现
    根据慢速查找发现,报错都走到了根据慢速查找的源码,我们发现,其报错最后都是走到__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_forward_handler,并没有找到该方法的实现,那么在源码中去掉一个下划线进行全局搜索_objc_forward_handler,最后发现默认执行的是objc_defaultForwardHandler方法。下面是源码实现。
// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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;

打印输出的就是如上方法为找到的错误提示。

动态决议

  • 苹果建议在慢速查找没有找到方法实现的时候,使用动态决议方法,可以算是补救崩溃的一个机会吧。
  • 这个补救崩溃的机会就在lookUpImpOrForward方法中
//如果没有找到方法实现,尝试方法解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //动态方法决议的控制条件,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
  • resolveMethod_locked源码实现
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //对象 -- 类
    if (! cls->isMetaClass()) { //类不是元类,调用对象的解析方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {//如果是元类,调用类的解析方法, 类 -- 元类
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //为什么要有这行代码? -- 类方法在元类中是对象方法,所以还是需要查询元类中对象方法的动态方法决议
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //如果方法解析中将其实现指向其他方法,则继续走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

所以如果没有实现的方法,我们可以通过resolveInstanceMethod进行一次补救,

  • 主要流程分为以下几个步骤

类不是元类,调用对象的解析方法,执行的是-(void) resolveInstanceMethod(id inst, SEL sel, Class cls)方法;
如果是元类,调用类的解析方法+ (BOOL)resolveClassMethod:(SEL)sel, 即类 -- 元类

  • 分析流程图
    动态解析流程图
  • 实例方法的动态决议
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //发送resolve_sel消息

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找say666
    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));
        }
    }
}

主要分为以下几个步骤:

  • 在发送resolveInstanceMethod消息前,需要查找cls类是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法

  • 如果没有,则直接返回

  • 如果,则发送resolveInstanceMethod消息再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法

  • 实例方法示例:say666未实现的方法,通过resolveInstanceMethod动态决议后由sayMaster进行实现。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        //获取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //获取sayMaster的实例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //获取sayMaster的丰富签名
        const char *type = method_getTypeEncoding(sayMethod);
        //将sel的实现指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}
  • 类方法的动态决议
    解决未实现类方法调用崩溃问题,比如上面的+(void)sayNB;方法添加如下代码
+ (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];
}

输出结果
根据结果我们发现没有再出现崩溃的现象,并且LGPerson打印了lgClassMethod方法。注意:resolveClassMethod类方法的重写需要注意一点,传入的cls不再是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法

动态决议相关优化

我们通过继承链可以知道,实例方法:类 -- 父类 -- 根类 -- nil,类方法元类 -- 根元类 -- 根类 -- nil

isa流程图.png

那么我们可以将动态决议方法,放在NSObject的分类里面,统一实现未实现的方法。当然我们需要注意的是,这样会拦截系统的方法,我们可以按照项目模块针对自定义类中方法统一方法名的前缀判断。

+ (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;
}

消息转发

如果方法的快速查找、慢速查找以及动态决议都找不到的情况下,就会进行一个消息转发,我们可以利用消息转发来做一些操作避免出现崩溃,这同样是苹果给我们的一个避免发生错误的机会。

  • 通过instrumentObjcMessageSends的方式查看发送消息的日志

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现,所以,在main中调用instrumentObjcMessageSends打印方法调用的日志信息,有以下两点准备工作
1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES
2、在main中通过extern 声明instrumentObjcMessageSends方法

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

可以根据源码提供的路径/tmp/msgSends查看日志

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

运行程序崩溃,按照路径可以找到如下文件

  • 崩溃日志路径


    崩溃日志路径
  • 崩溃日志内容


    崩溃日志内容

打开日志我们可以看到

  • 两次动态方法决议:resolveInstanceMethod方法
  • 两次消息快速转发:forwardingTargetForSelector方法
  • 两次消息慢速转发:methodSignatureForSelector + resolveInstanceMethod
  • 使用bt查看堆栈信息
    运行程序崩溃查看堆栈信息
  • 发现___forwarding___来自CoreFoundation
    ___forwarding___来自CoreFoundation
  • image list读取整个镜像文件,然后搜索CoreFoundation,查看其可执行文件的路径如下图所示
    CoreFoundation路径
  • 根据CoreFoundation路径找到该文件
    `CoreFoundation`文件
  • 通过反汇编工具hopper查看
    CoreFoundation文件拖入hopper

搜索__forwarding_prep_0___方法,通过跳转____forwarding___,我们可以看到该方法的伪代码了,如下所示

____forwarding___部分伪代码

通过操作我们可以先判断是否实现了forwardingTargetForSelector方法,如果没有响应,跳转至loc_64a67也就是如果快速转发没有响应,则进入慢速转发流程,
loc_64a67伪代码实现
查看是否实现methodSignatureForSelector方法,如果没有响应,跳转至loc_64e3,则直接报错,如果获取methodSignatureForSelector的方法签名为nil,也是直接报错
loc_64e3c伪代码

如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理
`methodSignatureForSelector`返回值`不为空逻辑处理伪代码实现

  • 消息转发机制流程图


    消息转发机制流程图

    消息转发的处理主要分为两部分:

【快速转发】当慢速查找,以及动态方法决议均没有找到实现时,进行消息转发,首先是进行快速消息转发,即走forwardingTargetForSelector方法如果返回消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程如果返回nil,则进入慢速消息转发

【慢速转发】执行到methodSignatureForSelector方法,如果返回的方法签名为nil,则直接崩溃报错

如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错

总结:消息转发有三种
  • forwardingTargetForSelector快速转发
    如果动态决议没有找到方法,则需要在LGPerson中重写forwardingTargetForSelector方法,将LGPerson的实例方法接收者指定为LGStudent的对象,代码如下
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //将消息的接收者指定为LGStudent,在LGStudent中查找say666的实现
    return [LGStudent alloc];
}

输出结果如下所示
forwardingTargetForSelector进行消息快速转发
  • methodSignatureForSelectorforwardInvocation慢速转发

如果快速转发没有找到方法的实现,就会进行慢速转发流程,代码如下

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    // GM  sayHello - anInvocation - 漂流瓶 - anInvocation
    anInvocation.target = [LGStudent alloc];
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}

输出结果如下所示,并且发现forwardInvocation方法中不对invocation进行处理,也不会崩溃报错

快速转发流程输出结果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342