美文网首页
消息动态决议

消息动态决议

作者: 浅墨入画 | 来源:发表于2021-08-01 21:54 被阅读0次

    前言

    前面我们分析了消息的查找流程快速查找流程慢速查找流程。如果这两种方式都没找到方法的实现,苹果给了两个建议

    • 动态方法决议:慢速查找流程未找到后,会执行一次动态方法决议
    • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发

    如果这两个建议都没有找到方法的实现,就会崩溃报错unrecognized selector sent to instance 0x600000c880d0

    定义LGPerson类,其中sayHello实例方法没有实现,main.m文件调用,崩溃如下

    <!-- LGPerson.h文件 -->
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson : NSObject
    - (void)sayHello;
    - (void)sayMaster;
    @end
    
    NS_ASSUME_NONNULL_END
    
    <!-- LGPerson.m文件 -->
    #import "LGPerson.h"
    
    @implementation LGPerson
    - (void)sayMaster {
        NSLog(@"%@ : %s",self,__func__);
    }
    @end
    
    image.png

    方法找不到的报错底层

    根据慢速查找源码,我们发现其报错后都是走到__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;
    

    这就是我们日常开发中最常见的错误:没有实现函数,运行程序,崩溃报错提示。探索流程总结

    • 由控制台打印信息unrecognized selector sent to instance为出发点,向下深挖方法的响应流程
    • 通过对LookUpImpOrForward的分析得出在慢速查找未找到的时候,给imp默认赋值forward_impobjc_msgForward_impcache
    • 通过对objc_msgForward_impcache的查找,发现在汇编中它只是一个中间函数,真正指向的是objc_msgForward
    • objc_msgForward的分析得出,方法内调用了objc_forward_handle获取返回值存到x17寄存器,再由TailCallFunctionPointer方法跳转到x17寄存器内真正指向的imp地址
    • _objc_forward_handler会给定默认的实现objc_defaultForwardHandle,当imp最终的查找流程全部走完的时候还未找到imp,那么此时就会进入objc_defaultForwardHandle函数,将错误信息打印出来。

    为了防止方法未实现的崩溃,下面我们从对象方法动态决议开始探索

    对象方法动态决议

    慢速查找流程未找到方法实现时,循环查找结束,接下来进入动态方法决议

    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
        const IMP forward_imp = (IMP)_objc_msgForward_impcache;
        IMP imp = nil;
    
        for (unsigned attempts = unreasonableClassCount();;) {
    
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                //当消息的快速查找、慢速查找流程都走完还找不到的情况下
                //imp会给一个默认值forward_imp,进入消息转发流程
                imp = forward_imp;
                break;
            }
            //找到了sel对应的imp
            if (fastpath(imp)) {
                goto done;
            }
        }
        /**
        * 如果遍历查找的过程找到了,会跳过此步骤,取到done分支,进行后续操作
        * 如果找不到,会进行下面这个算法,最终进入resolveMethod_locked函数
        * 此算法真正达到的目的为单例,保证一个lookUpImpOrForward
        * 只执行一次resolveMethod_locked
        */ 
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
        return imp;
    }
    
    位运算
    • &:按位与,是双目运算符。其功能是参与运算的两数各对应的二进位相与。相同位置数字都为1时,结果位才为1。
    • ^:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0
    • ^=:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0,将结果赋值为运算符左边。
    单例解读
    .macro MethodTableLookup
        
        SAVE_REGS MSGSEND
    
        // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
        // receiver and selector already in x0 and x1
        mov x2, x16
        mov x3, #3
        bl  _lookUpImpOrForward
    
        // IMP in x0
        mov x17, x0
    
        RESTORE_REGS MSGSEND
    
    .endmacro
    
    • 前提条件(初始值) behavior = 3 LOOKUP_RESOLVER = 2
    • 初次判断操作behavior & LOOKUP_RESOLVER = 3 & 2 = 0x11 & 0x10 = 0x10 = 2
    • 重置behaivor behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0x11 ^ 0x10 = 0x01 = 1
    • 再次进入判断操作(第二次至无限次)behavior & LOOKUP_RESOLVER = 1 & 2 = 0x01 & 0x10 = 0x00 = 0
    得出结论

    保证了每一个lookUpImpOrForward函数最多只能执行一次resolveMethod_locked(动态方法决议),直到behavior被重新赋值

    动态方法决议源码

    调用一个方法流程,先进入消息快速查询流程 -> 然后消息慢速查找流程,当底层源码已经查找了两遍依然找不到方法的实现,此时imp=nil,理论上来讲程序应该崩溃。但在开发者的角度,崩溃表示这个框架不稳定,系统很不友善。所以此框架决定再给你一次机会,为你提供一个返回自定义imp的机会,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:实例方法动态添加imp
    • resolveClassMethod:类方法动态添加imp
    • lookUpImpOrForwardTryCache,当完成添加之后,回到之前的慢速查找流程再来一遍
    类动态方法决议会进入resolveClassMethod,然后根据判断有可能会再次进入resolveInstanceMethod为什么?

    正常的类对象动态方法决议会进入resolveClassMethod,这点是毋庸置疑的,但是类方法查找过程是在元类中查找,那么通过isa的指向图中可以得知,类方法的查找过程也是有继承关系的,会一直向上找,找到superMetaClass,找到rootMetaClass,最终找到NSObject,到这一层所有的方法对于NSObject来讲都是实例方法所以会调用resolveInstanceMethod

    实例方法源码

    针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法

    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慢速查找流程查找实例方法
    崩溃修改

    针对实例方法sayHello未实现的报错,可以通过在类中重写resolveInstanceMethod类方法,并将其指向其他方法的实现。即在LGPerson中重写resolveInstanceMethod类方法,将实例方法sayHello的实现指向sayMaster方法实现,如下所示

    // LGPerson.m 文件添加如下resolveInstanceMethod方法
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(sayHello)) {
            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];
    }
    
    // 运行工程其打印如下
    2021-08-01 17:51:30.226277+0800 DDDDD[5106:762939] sayHello 来了
    2021-08-01 17:51:30.226734+0800 DDDDD[5106:762939] <LGPerson: 0x281ecc040> : -[LGPerson sayMaster]
    

    类方法的动态决议

    类方法动态决议源码

    /*********************************************************
    * 解析类方法
    * 调用+resolveClass 方法,寻找要添加到类cls 的方法。
    * cls 应该是一个元类。
    * 不检查该方法是否已经存在。
    *********************************************************/
    static void resolveClassMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        ASSERT(cls->isMetaClass());
    
        //当你为实现resolveClassMethod的时候,此处也不会进入return
        //因为系统给resolveClassMethod函数默认返回NO,即默认实现了
        if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
            // Resolver not implemented.
            return;
        }
        
        //nonmeta容错处理
        Class nonmeta;
        {
            mutex_locker_t lock(runtimeLock);
            nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
            if (!nonmeta->isRealized()) {
                _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                            nonmeta->nameForLogging(), nonmeta);
            }
        }
        //系统会在此处为你发送一个消息resolveClassMethod
        //当你的这个类检测了这个消息,并且做了处理
        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    
        //那么此时系统会重新查找,此函数最终会触发LookUpImpOrForward
        IMP imp = lookUpImpOrNilTryCache(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 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));
            }
        }
    }
    

    LGPerson类中添加类方法如下,其中sayHappy方法没有实现

    <!-- LGPerson.h文件 -->
    + (void)sayNB;
    + (void)sayHappy;
    
    <!-- LGPerson.m文件 -->
    + (void)sayNB {
        NSLog(@"%@ : %s",self,__func__);
    }
    
    // 添加类方法动态决议
    + (BOOL)resolveClassMethod:(SEL)sel{
        if (sel == @selector(sayHappy)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            
            IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
            Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
            const char *type = method_getTypeEncoding(lgClassMethod);
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
        }
        return [super resolveClassMethod:sel];
    }
    
    // 运行工程其打印如下
    2021-08-01 18:46:27.239762+0800 DDDDD[5295:774648] sayHappy 来了
    2021-08-01 18:46:27.239814+0800 DDDDD[5295:774648] LGPerson : +[LGPerson sayNB]
    

    注意: resolveClassMethod类方法的重写传入的cls不再是类,而是元类。可以通过objc_getMetaClass方法获取类的元类,原因是类方法在元类中是实例方法

    得出结论
    • 通过手动添加resolveInstanceMethod/resolveClassMethod可以防止方法未实现崩溃
    • 通过判断当前查找的sel动态添加一个新的imp
    • 此时sel(sayHello/sayHappy)对应实现的imp不再是sayHello/sayHappy,而是变成了我动态指定的sayMaster/sayNB
    优化

    日常开发中不可能每个类中都实现一份resolveInstanceMethod/resolveClassMethod,有没有更好的解决方案呢?通过方法慢速查找流程可以发现其查找路径有两条

    • 实例方法:类 -- 父类 -- 根类 -- nil
    • 类方法:元类 -- 根元类 -- 根类 -- nil

    它们的共同点是如果前面没找到,都会来到根类即NSObject中查找,所以我们可以将上述的两个方法整合在一起。通过NSObject添加分类的方式来统一处理,而且由于类方法的查找在其继承链中查找的也是实例方法,所以可以将实例方法和类方法统一放在resolveInstanceMethod方法中处理,如下所示

    @implementation NSObject (LG)
    
    //为实例对象创建一个IMP
    - (void) sayMaster {
        NSLog(@"%@ : %s",self,__func__);
    }
    
    //为元类对象创建创建一个IMP
    + (void) sayNB {
        NSLog(@"%@ - %s",self,__func__);
    }
    
    #pragma clang diagnostic push
    // 让编译器忽略错误
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(sayHello)) {
            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(sayHappy)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
            Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
            const char *type = method_getTypeEncoding(lgClassMethod);
            return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
        }
        return NO;
    }
    #pragma clang diagnostic pop
    @end
    
    // 运行工程其打印如下
    2021-08-01 19:20:31.446141+0800 DDDDD[5405:783621] sayHello 来了
    2021-08-01 19:20:31.446775+0800 DDDDD[5405:783621] <LGPerson: 0x2838f8080> : -[NSObject(LG) sayMaster]
    2021-08-01 19:20:31.446847+0800 DDDDD[5405:783621] sayHappy 来了
    2021-08-01 19:20:31.446881+0800 DDDDD[5405:783621] LGPerson - +[NSObject(LG) sayNB]
    

    这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中的实例方法
    当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验。

    aop和oop总结

    oop面向对象编程,不同类做不同事情,分工明确。
    • 优点:耦合度很低
    • 缺点:代码冗余,常规解决办法是提取,这样会有一个公共类。所有人对公共类进行集成,形成强依赖,也就代表着出现了强耦合
    aop:面向切面编程,是oop的延伸
    • 优点:对业务无侵入,通过动态方式将某些方法进行注入
    • 缺点:做了一些判断,执行了很多无关代码,包括系统方法,造成性能消耗,会打断apple的方法动态转发流程。

    消息转发流程引入

    在慢速查找的流程中,我们了解到如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发。但是我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法

    • 通过instrumentObjcMessageSends方式打印发送消息的日志

    objc4-818.2源码全局搜索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;
    }
    

    探索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,结束监听传递参数为NO
            instrumentObjcMessageSends(YES);
            [person sayHello];
            // 这里再次赋值为NO,表示只查看sayHello方法
            instrumentObjcMessageSends(NO);
        }
        return 0;
    }
    
    • 通过logMessageSend源码了解到消息发送打印信息存储在/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;
    }
    
    • 运行代码并前往/tmp/msgSends目录,发现有msgSends开头的日志文件,打开发现在崩溃前执行了以下方法
      两次动态方法决议:resolveInstanceMethod方法
      两次消息快速转发:forwardingTargetForSelector方法
      两次消息慢速转发:methodSignatureForSelector + resolveInvocation
    image.png

    相关文章

      网友评论

          本文标题:消息动态决议

          本文链接:https://www.haomeiwen.com/subject/zxghvltx.html