开篇
好好学习,不急不躁,每天进步一点点。
在上两篇文章中,讲解了objc_msgSend的消息发送流程;可以简单概括为4个步骤:
消息发送------> 查找imp ------> 缓存命中 ------>消息回调;
而这一系列的实现基础,基本是在函数方法已经实现的情况下,如果函数方法未实现,直接调用,应用将会崩溃,提示方法查找不到,那么在方法未实现的情况,如何实现动态消息,预防程序崩溃呢?
源码查找思路
创建一个类,和声明一个对象方法,但未实现该方法,则程序运行时,系统会提示错误,并且打印错误信息:
我们来简单分析一下,错误提示是如何出现的:
在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++源码内,找到错误提示的源码:
在很多时候,查看源码是非常吃力的,需要我们沉着冷静的,耐着性子一点一点的分析、琢磨,最终都是会有收获的;
动态方法
回到imp查找流程,在imp查找不到的时候,直接程序崩溃,那是非常不好的体验,而苹果工程师,为了显示自己的牛逼,也给我们容错的机会;那就来查看,牛逼哄哄的人,容错思路是怎样的吧。
在慢速查找函数lookUpImpOrForward
中,执行for
循环imp结束后,如果没有找到,将会执行下面的逻辑:
PS:后面将会讲解,为什么单个流程内,if条件只执行一次。
发送动态方法
分析 resolveMethod_locked 发送动态消息的流程:
resolveMethod_locked
resolveInstanceMethod
resolveInstanceMethod
方法,是通过类去判断是否存在的,所以这个方法是一个类方法,而不是对象方法;
所以在imp查找不到时,系统会发送resolve_sel消息,查询类是否实现resolveInstanceMethod
方法;
当程序执行resolveInstanceMethod
方法时,我们发现,在系统执行对象方法前,会先执行 resolveInstanceMethod 两次
;
- 第一次:动态方法执行,正常会调用一次;
- 第二次:消息转发完毕后,系统内部会再次发送消息,再执行一次:
执行两次是因为,如果在首次执行resolveInstanceMethod
过程中,系统在其他逻辑处动态添加了对象方法的话,那么就可以直接将方法的imp返回,而不用再执行动态决议的流程,这也是系统的一种容错机制;
接下来在实现resolveInstanceMethod
类方法,并且动态添加方法;
没有sayHi
方法,我们尝试将方法替换,让sayHi
方法,执行sayNB
的逻辑,程序执行是没有问题的。
类方法动态决议
系统根据cls是否为元类,进行不同的方法发送;cls为元类,执行resolveClassMethod
方法;
resolveClassMethod
这是一个类方法,写在元类里;
在元类中,类方法是以对象方法的形式存在,所以元类的对象方法,就是类的类方法;
在当前类中,实践resolveClassMethod
:
实现resolveClassMethod
,将未实现的sayHappy
方法,转换为sayKC
,从而避免系统异常;
在执行完resolveClassMethod
方法后,继续判断lookUpImpOrNilTryCache
是否存在,进而执行resolveInstanceMethod
方法,为什么系统在这里,又做一次对象动态消息发送呢?
还记得isa走位图吗,在isa走位图中,class的继承链,subclass --->superclass ---> root class,所以class里的方法走位,也是与它的继承链一致;
而类方法,保存在元类中,所以它既可以执行class的继承链,同时也会在元类中,以对象的形式,执行元类的继承链;所以存在执行两次的情况;
- 通过两个案例,我们明白,所有类里面的方法都会执行
resolveInstanceMethod
方法,而元类里面的方法,则会执行resolveClassMethod
方法,那么如果我们创建一个NSObject的元类,实现resolveInstanceMethod
方法,是不是就能做到所有类的方法获取不到时,都调用NSObject的容错处理呢?接下来我们实践试试:
而结果也正如我们所预期的一样,妥妥的没有问题;
小结
通过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 前往文件夹,查找文件信息:
前往文件夹,内部存在msgSends文件,这就是日志文件:(注意,如果是在源码内开启日志写入,日志文件内是没有日志信息的,需要新的工程才能查看日志文件)
通过日志文件可以看到,程序除了执行resolveInstanceMethod
,还执行了forwardingTargetForSelector
,methodSignatureForSelector
这两个函数:
这两个函数的作用是什么呢?又该如何使用?请等待后续分析......