iOS 底层探索篇 ——Runtime-消息转发

649 阅读6分钟

这是我参与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___。

在这里插入图片描述

点击进去,这里判断是否响应forwardingTargetForSelector方法,如果没有响应,跳转至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之前

在这里插入图片描述

到这里可以知道:第二次动态方法决议methodSignatureForSelectorforwardInvocation方法之间。

总结

objc_msgSend 的总体流程:

  • 快速查找-在类以及父类或者元类以及父元类的方法缓存中寻找方法的实现。
  • 慢速查找-在类以及父类或者元类以及父元类的方法列表中寻找方法的。
  • 动态消息决议- 如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法。
  • 快速转发- forwardingTargetForSelector进行消息接受者的处理。
  • 慢速转发- methodSignatureForSelector以及forwardInvocation进行方法签名的处理。
  • 如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance。