iOS objc_msgSend 动态方法决议

1,398 阅读7分钟

12 2.png

开篇

好好学习,不急不躁,每天进步一点点。

在上两篇文章中,讲解了objc_msgSend的消息发送流程;可以简单概括为4个步骤:

消息发送------> 查找imp ------> 缓存命中 ------>消息回调;

而这一系列的实现基础,基本是在函数方法已经实现的情况下,如果函数方法未实现,直接调用,应用将会崩溃,提示方法查找不到,那么在方法未实现的情况,如何实现动态消息,预防程序崩溃呢?

源码查找思路

创建一个类,和声明一个对象方法,但未实现该方法,则程序运行时,系统会提示错误,并且打印错误信息:

错误提示.png

我们来简单分析一下,错误提示是如何出现的:

在imp 慢速查找过程中,通过__objc_msgForward_impcache函数,默认给imp赋值,通过函数名查找,在汇编源码内,执行流程是这样的:

1、__objc_msgForward_impcache

2、__objc_msgForward

3、__objc_forward_handler

搜索 __objc_forward_handler 函数,发现方法只存在汇编源码内,但错误提示,却是在C/C++内,所以需要转换查询方式,objc_forward_handler 直接搜索该函数名称,在C/C++源码内,找到错误提示的源码:

错误提示.png

在很多时候,查看源码是非常吃力的,需要我们沉着冷静的,耐着性子一点一点的分析、琢磨,最终都是会有收获的;

动态方法

回到imp查找流程,在imp查找不到的时候,直接程序崩溃,那是非常不好的体验,而苹果工程师,为了显示自己的牛逼,也给我们容错的机会;那就来查看,牛逼哄哄的人,容错思路是怎样的吧。

在慢速查找函数lookUpImpOrForward中,执行for循环imp结束后,如果没有找到,将会执行下面的逻辑:

动态消息.png

PS:后面将会讲解,为什么单个流程内,if条件只执行一次。

发送动态方法

分析 resolveMethod_locked 发送动态消息的流程:

  • resolveMethod_locked

动态消息-第 2 页.png

  • resolveInstanceMethod 动态消息-第 3 页.png

动态消息-第 4 页.png

resolveInstanceMethod方法,是通过类去判断是否存在的,所以这个方法是一个类方法,而不是对象方法;

所以在imp查找不到时,系统会发送resolve_sel消息,查询类是否实现resolveInstanceMethod方法;


当程序执行resolveInstanceMethod方法时,我们发现,在系统执行对象方法前,会先执行 resolveInstanceMethod 两次

  • 第一次:动态方法执行,正常会调用一次;
  • 第二次:消息转发完毕后,系统内部会再次发送消息,再执行一次:

执行两次是因为,如果在首次执行resolveInstanceMethod过程中,系统在其他逻辑处动态添加了对象方法的话,那么就可以直接将方法的imp返回,而不用再执行动态决议的流程,这也是系统的一种容错机制;

截屏2021-08-04 下午6.23.27.png

接下来在实现resolveInstanceMethod类方法,并且动态添加方法;

截屏2021-08-04 下午6.48.56.png

没有sayHi方法,我们尝试将方法替换,让sayHi方法,执行sayNB的逻辑,程序执行是没有问题的。

截屏2021-08-04 下午6.49.36.png

类方法动态决议

系统根据cls是否为元类,进行不同的方法发送;cls为元类,执行resolveClassMethod方法;

动态消息-第 5 页.png

resolveClassMethod这是一个类方法,写在元类里;

在元类中,类方法是以对象方法的形式存在,所以元类的对象方法,就是类的类方法;

在当前类中,实践resolveClassMethod

截屏2021-08-06 上午10.29.35.png

实现resolveClassMethod,将未实现的sayHappy方法,转换为sayKC,从而避免系统异常;

截屏2021-08-06 上午10.31.45.png

在执行完resolveClassMethod方法后,继续判断lookUpImpOrNilTryCache是否存在,进而执行resolveInstanceMethod方法,为什么系统在这里,又做一次对象动态消息发送呢?

还记得isa走位图吗,在isa走位图中,class的继承链,subclass --->superclass ---> root class,所以class里的方法走位,也是与它的继承链一致;

对象gif.gif

而类方法,保存在元类中,所以它既可以执行class的继承链,同时也会在元类中,以对象的形式,执行元类的继承链;所以存在执行两次的情况;

类.gif

  • 通过两个案例,我们明白,所有类里面的方法都会执行resolveInstanceMethod方法,而元类里面的方法,则会执行resolveClassMethod方法,那么如果我们创建一个NSObject的元类,实现resolveInstanceMethod方法,是不是就能做到所有类的方法获取不到时,都调用NSObject的容错处理呢?接下来我们实践试试:

截屏2021-08-06 上午11.31.04.png

而结果也正如我们所预期的一样,妥妥的没有问题;

截屏2021-08-06 上午11.31.16.png

小结

通过sel无法获取imp时,系统提供动态的补救方案,如果app实现了补救方案,系统就不会崩溃;通过NSObject分类的模式,就可以监听整个工程中所有的方法;如果方法命名有规范,可以通过方法名称去区分模块,hook记录,作为日志文件,可以传递给后台监控;

运用runtime运行时模式去动态派发方法时,就容易出现方法imp不存在的情况。所以使用这种hook的思维去开发,是一种AOP面向切面编程方式思维;

而我们日常经常使用的研发方式,就是 OOP面向对象编程方式;OOP面向对象编程所有对象之间的分工是非常明确的,好处就是耦合度非常小,但是容易造成冗余代码,代码冗余就会提取封装,生成公共类,导致业务对公共类都有强依赖,强耦合,这就不符合架构思维,架构希望高内聚,低耦合

所以为了降低对业务代码的侵入,利用runtime动态派发的方式,把方法注入代码里,比如在NSObject分类中,注入resolveInstanceMethod,对QLYPerson类里的方法,没有任何影响;这就相当入整个切面都切入了,这就是AOP面向切面编程方式思维;

AOP面向切面编程方式的缺点:

  • 1、AOP思维对于初学者来说,是比较难理解的,多数人不容易形成这类思维;
  • 2、如上述例子,会在NSObject分类中,有比较多的if融于的判断条件,有一定的性能消耗;
  • 3、分类中resolveInstanceMethod会执行多次,有一定的性能消耗;
  • 4、resolveInstanceMethod 位于消息转发机制前,如果在此做了一刀切的程序逻辑,那么系统的接下来的补救逻辑,就无法实现; 所以,如果在这一层操作AOP,是不太友好的,应该在消息转发流程中,再操作AOP逻辑;

查看日志文件

如果动态方法决议没有实现,系统将判断imp是否等于默认值,如果相等,则返回nil,空的imp,然后执行下面的done流程,并且针对imp做任何的业务补充逻辑,那还有什么其他方式可以补救吗?

done流程内,做了log_and_fill_cache日志写入业务,通过logMessageSend函数将日志记录在本地的tem文件内,那我们是否可以通过查看日志文件,观测到程序后续的执行流程呢?

通过断点跟进,却发现断点不走入logMessageSend内,表明了日志文件未被开启。开启日志文件的字段为:objcMsgLogEnabled;追踪源码,定位到在instrumentObjcMessageSends函数内,对objcMsgLogEnabled进行赋值,接下来在程序中,添加日志开启常量:

**extern** **void** instrumentObjcMessageSends(**BOOL** flag);

在调用对象方法前,开启日志写入:

LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(**YES**);
[person sayHello];
instrumentObjcMessageSends(**NO**);

将程序运行,断点执行进入logMessageSend函数内,打开Finder,command + shift + G 前往文件夹,查找文件信息:

截屏2021-08-07 下午4.21.04.png

前往文件夹,内部存在msgSends文件,这就是日志文件:(注意,如果是在源码内开启日志写入,日志文件内是没有日志信息的,需要新的工程才能查看日志文件)

截屏2021-08-07 下午4.24.29.png

通过日志文件可以看到,程序除了执行resolveInstanceMethod,还执行了forwardingTargetForSelectormethodSignatureForSelector这两个函数:

截屏2021-08-07 下午4.28.10.png

这两个函数的作用是什么呢?又该如何使用?请等待后续分析......