这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
上文说到instrumentObjcMessageSends,那么这个方法是怎么来的呢。
在log_and_fill_cache里面有一个判断SUPPORT_MESSAGE_LOGGING。
进去logMessageSend方法,看到这里在往/tmp/msgSends-%d写入。
那么也就是说只要objcMsgLogEnabled
,那么就会写入。搜索一下objcMsgLogEnabled。发现在instrumentObjcMessageSends
里面对其进行赋值
所以,instrumentObjcMessageSends被找到了。
快速转发流程
上文中看到,msgSends中还会走到forwardingTargetForSelector.
在开发者文档中搜索forwardingTargetForSelector
。发现这里解释说,如果执行了这个方法,并且返回一个不为空的结果,这样这个返回的结果就会被当成新的消息接受者,然后在新的接受者上重新走一遍消息流程。如果返回的是自己的话,那么就会无限循环。
接下来实现一下:
运行一下,发现在奔溃前,会来到这个方法。
前面说到forwardingTargetForSelector可以切换接受者
,那么在另一个对象实现这个方法,然后在forwardingTargetForSelector里面把这个对象换成新的消息接受者。
运行一下,发现成功调用了LGTeacher里面的方法,并且程序不崩溃了。
那为什么方法不直接写到LGPerson里面呢。其实这里的作用是为了防止方法崩溃的,当所以经过快速查找,慢速查找,动态方法决议来到这里的时候,说明方法是没有实现的,那么我们就可以专门创建一个类,然后用这个类来添加方法,这样其他的类就依然还是安全健康的。
那么如果LGTeacher也没有方法实现,那么该怎么办呢?那么就来到了慢速转发流程,
慢速转发流程
之前看到forwardingTargetForSelector后面走的是methodSignatureForSelector
,在开发文档中搜索一下。
这个方法是伴随着forwardInvocation:
使用的
实现一下。
运行一下。
这里报错是因为没有实现forwardInvocation,实现一下。
运行一下还是报错。原因是因为没有返回签名。需要添加一下返回签名。点击NSMethodSignature
看如何创建签名,发现了一个signatureWithObjCTypes
方法。
添加一下:
运行发现不崩溃了,但是没有做任何的操作。
在系统层面里面,所有的方法函数都统称为系统的消息,也叫做事务。事务可做可不做,如果经过这么多流程,到了慢速转发,还提出了签名,那么系统就会把事务保存下来,但是不会触发。也就是说如果forwardInvocation方法中不对invocation进行处理,也不会崩溃报错
。验证一下是否真的有保存下来。
运行一下,发现确实有。
当然也可以处理invocation事务
来使方法生效。
那么如果我们在NSObject
对慢速转发进行处理,那么是不是所有的不存在的方法都不会报错了呢.实验一下。
这里确实不崩溃了,但是不建议这样做,因为这会造成很多内存和精力的浪费。
通过Hopper Disassembler/IDA反编译探索
如果不知道instrumentObjcMessageSends方法,那么也可以通过Hopper Disassembler
或者IDA
来进行反编译探索。
运行程序发现崩溃后,输入bt查看流程。发现doesNotRecognizeSelector是在CoreFoundation框架里面的,在其前面调用了___forwarding___ 和 _CF_forwarding_prep_0。
CoreFoundation是不完全开源的,所以在官网下载后,找不到___forwarding___
和 _CF_forwarding_prep_0
,这时候,就要用Hopper Disassembler/IDA
进行反编译探索
。
将CoreFoundation可执行文件
拖入Hopper后选择X86(64bits
),然后搜索__forwarding_
。发现在_CF_forwarding_prep_0之后会调用___forwarding___。
点击进去,这里判断是否响应forwardingTargetForSelecto
r方法,如果没有响应,跳转至loc_64a67
。即快速转发没有响应,进入慢速转发流程。响应的话则调用,调用如果返回的是空或者自身,则调转去loc_64a67,否则继续往下处理。
然后loc_64a67
判断是否是僵尸对象
,是的话就进入loc_64dc1
,否则就继续往下走。然后会判断是否可以响应methodSignatureForSelector
。这里就开始了慢速查找流程
。如果methodSignatureForSelector的返回值不为空,则会继续往下走走到_forwardStackInvocation
。_forwardStackInvocation是系统内部的方法,没有对外暴露所以无法调用。
继续往下走,看到loc_64c19里面判断是否响应forwardInvocation
。响应则继续往下走进行调用,不响应则跳去loc_64ec2。
消息转发机制流程图
resolveInstanceMethod 为什么走两次
为什么resolveInstanceMethod会走两次呢。先到resolveInstanceMethod调用的地方,打个断点然后运行。
发现确实是main中调用的方法。
在继续往下走,知道第二次进入的时候,在lldb中输入bt查看方法。发现这次的resolveInstanceMethod
是在_CF_forwarding_prep_0
之后调用的,所以是在_CF_forwarding_prep_0
->___forwarding___
->doesNotRecognizeSelector
里面做了一些操作之后进来的。
找到 doesNotRecognizeSelector 发起的地方
然后往调用loc_64e3c的地方查找,发现是methodSignatureForSelector没有响应的时候会进来。
而在doesNotRecognizeSelector
里面往下走,则走到了loc_64ec2
里面。看一下loc_64ec2
,发现会走到loc_64ed1
。
看一下loc_64ed1
,到这里整个流程走完。
点击 ____forwarding___.cold.4
,这里做一些回调,调用系统内部的方法然后走到libsystem,然后走到class_respondsToSelector_inst
里面。
搜索一下class_respondsToSelector_inst
,发现这里调用了lookUpImpOrNilTryCache
,而lookUpImpOrNilTryCache会走到lookUpImpOrForward
里面,所以这是为什么动态方法决议被调用了两次的原因。
通过代码推导
重新打开并打印forwardingTargetForSelector方法,如果resolveInstanceMethod在forwardingTargetForSelector之前,那么就是之前在无限递归,而在之后则说明第二次调用resolveInstanceMethod是在快速转发之后
运行一下,发现第二次resolveInstanceMethod 在forwardingTargetForSelector
之后调用的。说明可能是forwardingTargetForSelector调用或者之后调用的。
再来试一下慢速转发流程,发现resolveInstanceMethod是在methodSignatureForSelector
之后打印的。
再来试一下forwardInvocation
,看到resolveInstanceMethod在forwardInvocation之前
到这里可以知道:第二次动态方法决议
在 methodSignatureForSelector
和 forwardInvocation
方法之间。
总结
objc_msgSend 的总体流程:
- 快速查找-在类以及父类或者元类以及父元类的方法缓存中寻找方法的实现。
- 慢速查找-在类以及父类或者元类以及父元类的方法列表中寻找方法的。
- 动态消息决议- 如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法。
- 快速转发-
forwardingTargetForSelector
进行消息接受者的处理。 - 慢速转发-
methodSignatureForSelector
以及forwardInvocation
进行方法签名的处理。 - 如果转发之后也没有,则程序直接报错崩溃
unrecognized selector sent to instance。