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

1,350 阅读6分钟
  • 准备工作

    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);
     }
    
    • 流程分析
    • 关于实例方法的处理
      • 源码解析
      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];
      }
      
      此时我们在看控制台的打印信息
    • 关于类方法的处理
      • 源码解析 同样的还是这一块代码,但是会走else里面的代码
      • 源码解读
        从上述代码可以看出,如果类方法找不到,这里会有两个地方可以补救(实例方法只有一个地方resolveInstanceMethod)一个是重写resolveClassMethod方法还一个是在元类中重写resolveInstanceMethod方法,代码如下:
    • 关于崩溃处理的优化方法
      • 明确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___如下看伪代码发现首先判断forwardingTargetForSelector方法有没有实现,如果没有实现就跳到loc_64a67(慢速转发)有实现但是返回值是nil的时候直接报错然后再判断methodSignatureForSelector方法 有没有实现,没有实现直接报错了同样的如果返回值是nil也是会直接报错,如果不为空的话则在forwardInvocation方法中对invocation进行处理
    • 快速转发
      • forwardingTargetForSelector
      • 崩溃处理 实现forwardingTargetForSelector方法并返回对应的消息接受者(消息接受者中要有同名方法的实现)如下代码:
    • 慢速转发
      • methodSignatureForSelector
      • forwardInvocation
      • 崩溃处理 首先实现methodSignatureForSelector返回方法签名,然后再实现forwardInvocation方法 也可以处理invocation,修改target然后触发事务
    • 消息转发流程图