日记2 运行时,方法的发送、查找和转发

287 阅读5分钟

1. 方法:

方法缓存是在cache_t,cache读取流程,即objc_msgSendcache_getImp

2. 运行时:

是什么:Runtime是一套API,由c、c++、汇编一起写成的,为OC提供了运行时.
Runtime版本Runtime有两个版本——LegacyModern,

Legacy(``-old__OBJC__), Modern(-new__OBJC2__)

Runtime调用方式:

  • Runtime API,如 sel_registerName(),class_getInstanceSize
  • NSObject API,如 isKindOf()
  • OC上层方式,如 @selector()

3. 方法的本质是通过objc_msgSend发送消息,id是消息接收者,SEL是方法编号.

注意:如果外部定义了C函数并调用如void sayHello() {},在clang编译之后还是sayHello()而不是通过objc_msgSend去调用.因为发送消息就是找函数实现的过程,而C函数可以通过函数名——指针就可以找到.

实现上图, 这其中需要注意两点:

  • 1、直接调用objc_msgSend,需要导入头文件#import <objc/message.h>

  • 2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错

2. 方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找.

二、消息查找流程

消息查找流程其实是通过上层的方法编号sel发送消息objc_msgSend找到具体实现imp的过程

objc_msgSend是用汇编写成的,至于为什么不用C而用汇编写,是因为:

  • C语言不能通过写一个函数,保留未知的参数,跳转到任意的指针,而汇编有寄存器
  • 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别

总结:

  • 对于对象方法(即实例方法),即在类中查找,其慢速查找的父类链是:类--父类--根类--nil

  • 对于类方法,即在元类中查找,其慢速查找的父类链是:元类--根元类--根类--nil

  • 如果快速查找、慢速查找也没有找到方法实现,则尝试动态方法决议

  • 如果动态方法决议仍然没有找到,则进行消息转发

动态方法决议

  • 判断类是否是元类
    • 如果是,执行实例方法的动态方法决议resolveInstanceMethod
    • 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为,则在元类实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程


④ 优化方案

上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条

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

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

当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验.

那么把所有崩溃都在NSObjct分类中处理,加以前缀区分业务逻辑,岂不是美滋滋?错!

  • 统一处理起来耦合度高
  • 逻辑判断多
  • 可能在NSObjct分类动态方法决议之前已经做了处理
  • SDK封装的时候需要给一个容错空间

因此前面的 ④ 优化方案 也不是一个最完美的解决方案.那么,这也不行,那也不行,那该怎么办?放心,苹果爸爸已经给我们准备好后路了!

3. 消息转发机制

msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法

  • 两次动态方法决议:resolveInstanceMethod方法

  • 两次消息快速转发:forwardingTargetForSelector方法

  • 两次消息慢速转发:methodSignatureForSelector + resolveInvocation

快速转发流程解决崩溃  -  forwardingTargetForSelector

如下代码就是通过快速转发解决崩溃——即TCJPerson实现不了的方法,转发给TCJStudent去实现(转发给已经实现该方法的对象)
也可以直接不指定消息接收者,直接调用父类的该方法,如果还是没有找到,则直接报错

慢速转发流程解决崩溃

慢速转发流程就是先methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过对NSInvocation来实现消息的转发

其实也可以对forwardInvocation方法中的invocation不进行处理,也不会崩溃报错

总结:

到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下

  • 【快速查找流程】首先,在类的缓存cache中查找指定方法的实现
  • 【慢速查找流程】如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
  • 【动态方法决议】如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
  • 【消息转发】如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
  • 如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance