消息流程分析之 动态方法决议 & 消息转发

  • 准备工作
    1. objc4-781可编译源码 github.com/Bore-TuDou/…
    2. 创建一个TDPerson 里面有一个没有实现的实例方法sayNB和一个没有实现的类方法sayHappy,然后在创建一个TDStudent继承TDPerson,然后TDStudent中有一个没有实现的实例方法say666和一个没有实现的类方法say888
  • 动态方法决议 & 消息转发执行时机

    上面一篇文章有提到过慢速查找之后还没有找到辉县判断是否有执行过动态方法决议如果如果没有执行则进入到动态方法决议,如果已经执行了一次动态方法决议,则进入到消息转发,如果动态方法决议未被实现,同时消息转发也未被处理最后就会报错,所以在程序崩溃之前苹果还是给了两次补救的机会:

    1. 动态方法决议
    2. 消息转发
  • 动态方法决议
    • 源码分析
    static NEVER_INLINE IMP
     resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
     {
         runtimeLock.assertLocked();
         ASSERT(cls->isRealized());
    
         runtimeLock.unlock();
    
         if (! cls->isMetaClass()) {
             //如果当前传入的cls是一个类的话则调用对象的解析方法
             // try [cls resolveInstanceMethod:sel]
             resolveInstanceMethod(inst, sel, cls);
         } 
         else {
             //如果当前传入的cls是一个元类的话则调用类的解析方法
             // 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);
     }
    
    • 流程分析

    • 关于实例方法的处理

      • 源码解析

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这里有一个iOS交流群:[891488181]不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    ```
    static void resolveInstanceMethod(id inst, SEL sel, Class cls)
    {
        runtimeLock.assertUnlocked();
        ASSERT(cls->isRealized());
        SEL resolve_sel = @selector(resolveInstanceMethod:);

        //一个简单的容错处理如果没有查找到resolveInstanceMethod方法的实现直接return
        //正常情况下resolveInstanceMethod都有实现的
        if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
            // Resolver not implemented.
            return;
        }

        BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
        //发送resolve_sel消息
        bool resolved = msg(cls, resolve_sel, 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(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`方法,然后程序会再次查找方法,当然重写了`resolveInstanceMethod`方法但是里面没有相关的实现最后还是会崩溃,如果想要防止崩溃可以再`resolveInstanceMethod`方法中将`say666`方法的实现指向一个实现了的方法如下代码,就是讲`say666`的实现指向了`sayGooyBye`,然后在加入到方法列表,然后在查找根据`say666`就能查到`sayGoodBye`的实现

    ```
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(say666)) {
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            //获取sayGoodBye方法的imp
            IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
            //获取sayGoodBye的实例方法
            Method sayMethod  = class_getInstanceMethod(self, @selector(sayGoodBye));
            //获取sayGoodBye的方法签名
            const char *type = method_getTypeEncoding(sayMethod);
            //将sel的实现指向sayGoodBye 并加入到方法列表
            return class_addMethod(self, sel, imp, type);
        }
        return [super resolveInstanceMethod:sel];
    }
    ```

    此时我们在看控制台的打印信息![](https://upload-images.jianshu.io/upload_images/25061170-e50d5da5957de404.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


*   **关于类方法的处理**
    *   源码解析![](https://upload-images.jianshu.io/upload_images/25061170-b393287d404e9cc5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


    同样的还是这一块代码,但是会走else里面的代码
    *   源码解读
        从上述代码可以看出,如果类方法找不到,这里会有两个地方可以补救(实例方法只有一个地方`resolveInstanceMethod`)一个是重写`resolveClassMethod`方法还一个是在元类中重写`resolveInstanceMethod`方法,代码如下:![](https://upload-images.jianshu.io/upload_images/25061170-961aba3f7e7e041b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

*   **关于崩溃处理的优化方法**
    *   明确`resolveInstanceMethod`方法和`resolveClassMethod`方法的查找路径
        实例方法:类->父类->根类->nil
        类方法: 元类->根元类->根类->nil
    *   代码实现及思路
        从上面可以得知实例方法和类方法查找路径上都会经过根类,且根类是最后一环,所以我们只要创建一个根类的分类然后重写`resolveInstanceMethod`方法和`resolveClassMethod`方法就能做到拦截所有崩溃方法,如下代码实现:

        ```
        //
        //  NSObject+cash.m
        //  KCObjc
        //
        //  Created by huahua on 2020/11/15.
        //

        #import "NSObject+cash.h"
        #import <objc/runtime.h>

        @implementation NSObject (cash)

        +(BOOL)resolveClassMethod:(SEL)sel{
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            if (sel == @selector(say888)) {
        //        NSLog(@"%@ 来了", NSStringFromSelector(sel));
                //获取sayGoodBye方法的imp
                IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
                //获取sayGoodBye的实例方法
                Method sayMethod  = class_getInstanceMethod(self, @selector(sayGoodBye));
                //获取sayGoodBye的方法签名
                const char *type = method_getTypeEncoding(sayMethod);
                //将sel的实现指向sayGoodBye 并加入到方法列表
                return class_addMethod(objc_getMetaClass("TDStudent"), sel, imp, type);
            }
            return NO;
        }

        +(BOOL)resolveInstanceMethod:(SEL)sel{
            NSLog(@"%@ 来了", NSStringFromSelector(sel));
            if (sel == @selector(say888)) {
        //        NSLog(@"%@ 来了", NSStringFromSelector(sel));
                //获取sayGoodBye方法的imp
                //因为类方法是元类的实例方法所以这里要传入元类
                IMP imp = class_getMethodImplementation(objc_getMetaClass("TDStudent"), @selector(sayhai));
                //获取sayGoodBye的实例方法
                Method sayMethod  = class_getInstanceMethod(objc_getMetaClass("TDStudent"), @selector(sayhai));
                //获取sayGoodBye的方法签名
                const char *type = method_getTypeEncoding(sayMethod);
                //将sel的实现指向sayGoodBye 并加入到方法列表
                return class_addMethod(objc_getMetaClass("TDStudent"), sel, imp, type);
            }else if(sel == @selector(say666)){
                IMP imp = class_getMethodImplementation(self, @selector(sayGoodBye));
                //获取sayGoodBye的实例方法
                Method sayMethod  = class_getInstanceMethod(self, @selector(sayGoodBye));
                //获取sayGoodBye的方法签名
                const char *type = method_getTypeEncoding(sayMethod);
                //将sel的实现指向sayGoodBye 并加入到方法列表
                return class_addMethod(self, sel, imp, type);
            }
            return NO;
        }

        @end

        ```
  • 消息转发

    单纯的阅读源码你会发现源码上显示的是如果动态决议已经执行过一次了,就会直接报错,但是其实苹果还给了一个消息换发的机会

    • 查找消息转发的流程方法

      • 通过instrumentObjcMessageSends方式打印发送消息的日志
        首先看源码我们知道如果执行过动态方法决议之后回来到log_and_fill_cache方法

        点进log_and_fill_cache查看

        发现log_and_fill_cache方法的作用就是方法缓存和是否调用logMessageSend方法再看logMessageSend方法源码实现

        从这一步可以确认log_and_fill_cache方法就是一个方法缓存和判断是否需要打印日志的一个方法,在打印日志的条件objcMsgLogEnabled && implementerimplementer是传入的类(类或者元类)所以只要objcMsgLogEnabled为true就能打印日志,再看objcMsgLogEnabled

        发现是在instrumentObjcMessageSends方法中赋值的,所以在main函数中调用一下instrumentObjcMessageSends方法,将objcMsgLogEnabled赋值成true(这里注意需要先声明instrumentObjcMessageSends方法)如下代码:

        ,再去/tmp文件夹中找到对应的日志文件

      • 通过hopper/IDA反编译
        首先查看出错的堆栈信息:

        然后通过image list指令查找整个镜像文件的执行路径然后搜索CoreFountion,查找到其路径并找到对应的可执行文件

       拖进hopper然后左边栏搜索___forwarding___如下[![](https://img-blog.csdnimg.cn/img_convert/9fa7c3d9a920fb5b9da4a03d2c99005e.png)
      
      看伪代码发现首先判断`forwardingTargetForSelector`方法有没有实现,如果没有实现就跳到`loc_64a67`(慢速转发)有实现但是返回值是nil的时候直接报错![](https://img-blog.csdnimg.cn/img_convert/5cc8ca153fff6bd8dcdbdaf55f92c3d5.png)
      
      然后再判断`methodSignatureForSelector`方法 有没有实现,没有实现直接报错了![](https://img-blog.csdnimg.cn/img_convert/c2337bd25c9035267537b90fa378c39a.png)
      

      同样的如果返回值是nil也是会直接报错,如果不为空的话则在`forwardInvocation`方法中对`invocation`进行处理![](https://img-blog.csdnimg.cn/img_convert/48e19efc34f2362c632001d803788b47.png)
      
    • 快速转发

      • forwardingTargetForSelector
      • 崩溃处理 实现forwardingTargetForSelector方法并返回对应的消息接受者(消息接受者中要有同名方法的实现)如下代码:
    • 慢速转发

      • methodSignatureForSelector

      • forwardInvocation

      • 崩溃处理 首先实现methodSignatureForSelector返回方法签名,然后再实现forwardInvocation方法

        也可以处理invocation,修改target然后触发事务

    • 消息转发流程图

作者:Potato_土豆
链接:https://juejin.im/post/6895755250272534541

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值