OC底层原理10-消息动态决议

406 阅读6分钟

前言

上一篇我们打印了 unrecognized经典的崩溃信息,通过全局搜索doesNotRecognizeSelector或者unrecognized selector sent to instance,在源码中搜索方式查找到底层源码的实现,在方法查找流程中如果最后imp还是没有查找到,会调用forward_imp

Xnip2021-07-04_17-20-19.jpg forward_imp = _objc_msgForward_impcache,源码查看下 _objc_msgForward_impcache的底层实现,全局搜索_objc_msgForward_impcache

Xnip2021-07-04_17-21-56.jpg

Xnip2021-07-04_17-23-06.jpg

  • __objc_msgForward_impcache底层是汇编实现,主要代码 b __objc_msgForward
  • __objc_msgForward中TailCallFunctionPointer是个宏,前面探究过就是跳转imp。x17寄存器存放的是imp,从汇编中可以看出跟x17有关系的就是__objc_forward_handler
  • 全局搜索__objc_forward_handler 汇编中没有具体的实现,那就不在汇编中,可能在C/C++源码中,全局搜索objc_forward_handler,源码如下

Xnip2021-07-04_17-24-10.jpg

_objc_fatal中报错熟悉不,经典的崩溃信息报错方法没有实现

在快速和慢速查找流程过程中没有找到imp,难道就直接崩溃,不给一次机会的嘛。不行必须给次机会,不然我不服气,系统还是干不过我哈,给了次机会就是动态方法决议

动态方法决议

在探究慢速查找流程lookUpImpOrForward中,如果没有查找到imp就会走动态方法决议流程resolveMethod_locked

Xnip2021-07-04_17-26-23.jpg

下面看看resolveMethod_locked动态方法决议到底干了什么,源码如下

Xnip2021-07-04_17-27-34.jpg

  • 首先判断cls是否是元类
  • 如果不是元类只是普通类,那么说明调用的实例方法跳转resolveInstanceMethod流程
  • 如果是元类,那么说明调用的是类方法跳转resolveClassMethod流程
  • lookUpImpOrForwardTryCache快速查找和慢速查找sel对应的imp 然后返回imp

resolveInstanceMethod方法

Xnip2021-07-04_17-31-07.jpg

  • 首先创建resolveInstanceMethod 的SEL resolve_sel
  • 根据lookUpImpOrNilTryCache (cls, resolve_sel, cls->ISA(true))知道resolveInstanceMethod是类方法,通过快速和慢速查找流程查找resolve_sel对应的imp,缓存resolveInstanceMethod方法
  • 直接通过msg(cls, resolve_sel, sel)给类发送消息,从这里也能看到resolveInstanceMethod是类方法

lookUpImpOrNilTryCache(inst, sel, cls)快速和慢速查找流程

  • 通过lookUpImpOrNilTryCache来确定resolveInstanceMethod方法中有没有实现sel对应的imp
  • 如果实现了,缓存中没有,进入lookUpImpOrForward查找到sel对应imp插入缓存,调用imp查找流程结束
  • 如果没有实现,缓存中没有,进入lookUpImpOrForward查找,sel没有查找到对应的imp,此时imp = forward_imp,动态方法决议只调用一次,此时会走done_unlock和done流程,既sel和forward_imp插入缓存,进行消息转发

##resolveClassMethod方法

Xnip2021-07-04_17-33-59.jpg

  • resolveClassMethod在NSobject中已经实现,只要元类初始化就可以了,目的是缓存在元类中
  • 调用resolveClassMethod类方法,目的是实现可能resolveClassMethod``方法中动态实现sel对应的imp
  • imp = lookUpImpOrNilTryCache(inst, sel, cls) 缓存sel对应的imp,不管imp有没有动态添加,如果没有缓存的就是forward_imp

lookUpImpOrNilTryCache方法

lookUpImpOrNilTryCache方法名字,可以理解就是查找imp或者nil尽可能的通过查询cache的方式,在resolveInstanceMethod方法和resolveClassMethod方法都调用lookUpImpOrNilTryCache

Xnip2021-07-04_17-35-34.jpg

Xnip2021-07-04_17-36-28.jpg

首先最后一个参数默认是behavior = 0,LOOKUP_NIL = 4, behavior|LOOKUP_NIL 大于等于LOOKUP_NIL

Xnip2021-07-04_17-38-30.jpg

判断cls是否初始化一般都会初始化的

缓存中查找

  • 在缓存中查找sel对应的imp
  • 如果imp存在跳转done流程
  • 判断是否有共享缓存给系统底层库用的
  • 如果缓存中没有查询到imp,进入慢速查找流程

慢速查找流程

  • 慢速查找流程中,behavior= 4 ,4 & 2 = 0进入动态方法决议,所以不会一直循环
  • 最重要的如果没有查询到此时imp= forward_imp,跳转lookUpImpOrForward中的done_unlock和done流程,插入缓存,返回forward_imp

done流程

  • done流程: (behavior & LOOKUP_NIL) 且 imp = _objc_msgForward_impcache,如果缓存中的是forward_imp,就直接返回nil,否者返回的imp,LOOKUP_NIL条件就是来查找是否动态添加了imp还有就是将imp插入缓存

lookUpImpOrNilTryCache的主要作用通过LOOKUP_NIL来控制插入缓存,不管sel对应的imp有没有实现,还有就是如果imp返回了有值那么一定是在动态方法决议中动态实现了imp

resolveInstanceMethod实例探究

Xnip2021-07-04_20-09-29.jpg 在崩溃之前确实调用了resolveInstanceMethod方法

疑问:为什么会调用两次resolveInstanceMethod方法呢 第一次是走动态方法决议系统自动向resolveInstanceMethod发送消息,那么第二次是怎么调用的呢?

标记

动态添加sayTest2方法

Xnip2021-07-04_20-36-05.jpg

在main函数里面 还是调用sayTest方法 通过动态添加的sayTest2 结果打印sayTest2

Xnip2021-07-04_20-37-39.jpg

  • resolveInstanceMethod只调用一次,因为动态添加了sayTest2方法lookUpImpOrForwardTryCache直接获取imp,直接调用imp,查找流程结束
  • 崩溃也解决了动态方法决议系统给了一次机会
  • 具体流程:resolveMethod_locked--> resolveInstanceMethod --> 调用resolveInstanceMethod --> lookUpImpOrNilTryCache(inst, sel, cls) --> lookUpImpOrForwardTryCache--> 调用imp

resolveClassMethod实例探究

实现resolveClassMethod方法

Xnip2021-07-04_20-48-57.jpg

在main函数里面调用sayHello 通过动态添加的sayKC 结果打印sayKC

Xnip2021-07-04_20-52-45.jpg

Xnip2021-07-04_20-54-22.jpg

  • 在崩溃之前确实调用了resolveClassMethod方法,而且调用了两次,调用两次的逻辑和resolveInstanceMethod方法调用两次是一样的
  • 调用resolveClassMethod以后,会去查找lookUpImpOrNilTryCache有没有具体动态实现sel对应的imp,元类的缓存中此时有sel对应的imp,这个imp是forward_imp。lookUpImpOrNilTryCache里面有判断直接返回nil,此时直接到resolveInstanceMethod查找,因为类方法实际上就是元类中的实例方法
  • 如果最后还是没有实现lookUpImpOrForwardTryCache获取到forward_imp进入消息转发流程

resolveClassMethod特殊之处

标记

整合动态方法决议

resolveClassMethod方法中如果没有动态添加类方法,会调用元类中的resolveInstanceMethod。那么能不能把resolveInstanceMethod写到一个公用类中,使类方法和实例方法都能调用

  • 实例方法查找流程:对象 --> 类 -->直到根类(NSObject) --> nil
  • 类方法查找流程:类 --> 元类 -->直到根类(NSObject) --> nil

到最后都找到NSObject类中,所以这个公用类就是NSObject分类

Xnip2021-07-04_21-13-12.jpg

实例方法是类方法调用,系统都自动调用了resolveInstanceMethod方法,和上面探究的吻合。 动态方法决议优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页
  • 如果项目中是不同的模块你可以根据命名不同,进行业务的区别
  • 这种方式叫切面编程熟成AOP

AOP和OOP的区别

  • OOP:实际上是对对象的属性和行为的封装,功能相同的抽取出来单独封装,强依赖性,高耦合
  • AOP:是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护,依赖性小,耦合度小,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP更像一个三维的纵轴,平面内的各个类有共同逻辑的通过AOP串联起来,本身平面内的各个类没有任何的关联

消息转发

快速和慢速查找流程没有查询到,动态决议方法也没有查找到,下面就会进入消息转发流程,但是在objc4-818.2源码中没有发现相关的源码,CoreFunction提供的源码也不详细查询不到。苹果还是提供了日志辅助功能

日志辅助

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,进入logMessageSend 看到源码的实现