iOS Runtime必看faceruntime.runloop

iOS之武功秘籍⑥:Runtime之方法与消息

2021-02-22  本文已影响0人  長茳

iOS之武功秘籍 文章汇总

写在前面

上文说到cache_t缓存的是方法,我们分析了cache的写入流程,在写入流程之前,还有一个cache读取流程,即objc_msgSendcache_getImp.那么方法又是什么呢?这一切都要从Runtime开始说起...

本节可能用到的秘籍Demo

一、Runtime

① 什么是Runtime?

Runtime是一套API,由c、c++、汇编一起写成的,为OC提供了运行时.

② Runtime版本

Runtime有两个版本——LegacyModern苹果开发者文档都写得清清楚楚

源码中-old__OBJC__代表Legacy版本,-new__OBJC2__代表Modern版本,以此做兼容

③ Runtime的作用及调用

Runtime底层经过编译会提供一套API和供FrameWorkService使用

Runtime调用方式:

原来平常在用的这么多方法都是Runtime啊,那么方法究竟是什么呢?

二、方法的本质

① 研究方法

通过clang编译成cpp文件可以看到底层代码,得到方法的本质

② 代码转换

即可以理解为((类型强转)objc_msgSend)(对象, 方法调用)

③ 方法的本质

方法的本质是通过objc_msgSend发送消息,id是消息接收者,SEL是方法编号.

注意:如果外部定义了C函数并调用如void sayHello() {},在clang编译之后还是sayHello()而不是通过objc_msgSend去调用.因为发送消息就是找函数实现的过程,而C函数可以通过函数名——指针就可以找到.

为了验证,通过objc_msgSend方法来完成[person sayHello]的调用,查看其打印是否是一致.

其打印结果如下,发现是一致的,所以 [person sayHello]等价于objc_msgSend(person,sel_registerName("sayHello"))

这其中需要注意两点:

④ 向不同对象发送消息

子类TCJTeacher有实例方法sayHellosayNB, 类方法sayNC

父类TCJPerson有实例方法sayHellosayCode, 类方法sayNA

① 发送实例方法

消息接收者——实例对象

② 发送类方法

③ 对象方法调用-实际执行是父类的实现

注意前面的细节:父类TCJPerson中实现了sayHello方法,而子类TCJTeacher没有实现sayHello方法.现在我们可以尝试让teacher调用sayHello执行父类中实现,通过objc_msgSendSuper实现.

因为objc_msgSend不能向父类发送消息,需要使用objc_msgSendSuper,并给objc_super结构体赋值(在objc2中只需要赋值receiversuper_class)

receiver——实例对象;super_class——父类类对象

发现不论是[teacher sayHello]还是objc_msgSendSuper都执行的是父类中sayHello的实现,所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找.

④ 向父类发送实例方法

receiver——实例对象;super_class——父类类对象

⑤ 向父类发送类方法

receiver——类对象;super_class——父类元类对象

三、消息查找流程

消息查找流程其实是通过上层的方法编号sel发送消息objc_msgSend找到具体实现imp的过程

objc_msgSend是用汇编写成的,至于为什么不用C而用汇编写,是因为:

① 开始查找

打开objc4源码,由于主要研究arm64结构的汇编实现,来到objc-msg-arm64.s,先附上其汇编整体执行的流程图



p0表示0寄存器的指针,x0表示它的值,w0表示低32位的值(不用过多在意)

查看GetClassFromIsa_p16定义,主要就是进行isa & mask得到class操作

② 快速查找流程

CacheLookup开始了快速查找流程(此时x1selx16class

以下是整个快速查找过程值的变化过程流程图

③ 慢速查找流程

① 慢速查找-汇编部分

在快速查找流程中,如果没有找到方法实现,无论是走到CheckMiss还是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数

验证
上述汇编的过程,可以通过汇编调试来验证

从上可以看出最后走到的就是lookUpImpOrForward,此时并不是汇编实现.

注意

② 慢速查找-C/C++部分

根据汇编部分的提示,全局续搜索lookUpImpOrForward,最后在objc-runtime-new.mm文件中找到了源码实现,这是一个c实现的函数

其整体的慢速查找流程如图所示

慢速流程主要分为几个步骤:

以上就是方法的慢速查找流程,下面在分别详细解释二分查找原理 以及 父类缓存查找详细步骤

getMethodNoSuper_nolock方法:二分查找方法列表
查找方法列表的流程如下所示 其二分查找核心的源码实现如下

算法原理简述为:从第一次查找开始,每次都取中间位置,与想查找的key的value值作比较,如果相等,则需要排除分类方法,然后将查询到的位置的方法实现返回,如果不相等,则需要继续二分查找,如果循环至count = 0还是没有找到,则直接返回nil,如下所示:

以查找TCJPerson类的sayHello实例方法为例,其二分查找过程如下

cache_getImp方法:父类缓存查找

cache_getImp方法是通过汇编_cache_getImp实现,传入的$0GETIMP,如下所示

总结

常见方法未实现报错源码
如果在快速查找、慢速查找、方法解析流程中,均没有找到实现,则使用消息转发,其流程如下

消息转发会实现

看着objc_defaultForwardHandler有没有很眼熟,这就是我们在日常开发中最常见的错误:没有实现函数,运行程序,崩溃时报的错误提示.

🌰:定义TCJPerson父类,其中有sayNB实例方法 和 sayHappay类方法

定义子类:TCJStudent类,有实例方法sayHellosayMaster,类方法sayObjc,其中实例方法sayMaster未实现.

main中 调用TCJStudend的实例方法sayMaster ,运行程序报错,提示方法未实现,如下所示

下面,我们来讲讲如何在崩溃前,如何操作,可以防止方法未实现的崩溃.

四、动态方法解析

慢速查找流程未找到方法实现时,首先会尝试一次动态方法决议,其源码实现如下:

主要分为以下几步

其流程如下

① 实例方法

针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会,即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法,其源码如下

主要分为以下几个步骤:

② 崩溃修改--动态方法决议

针对实例方法say666未实现的报错崩溃,可以通过在中重写resolveInstanceMethod类方法,并将其指向其他方法的实现,即在TCJPerson中重写resolveInstanceMethod类方法,将实例方法say666的实现指向sayMaster方法实现,如下所示

假如我们在resolveInstanceMethod类方法中,不指向其他方法的实现,它会来两次,为什么会这样呢?我们在后面在解释...

③ 类方法

针对类方法,与实例方法类似,同样可以通过重写resolveClassMethod类方法来解决前文的崩溃问题,即在TCJPerson类中重写该方法,并将sayNB类方法的实现指向类方法sayHappy

resolveClassMethod类方法的重写需要注意一点,传入的cls不再是类而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为类方法在元类中是实例方法.

④ 优化方案

上面的这种方式是单独在每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条

它们的共同点是如果前面没找到,都会来到根类即NSObject中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是可以的,可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其继承链,查找的也是实例方法,所以可以将实例方法 和 类方法的统一处理放在resolveInstanceMethod方法中,如下所示

这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中是实例方法.

当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验.

⑤ 动态方法决议总结

那么把所有崩溃都在NSObjct分类中处理,加以前缀区分业务逻辑,岂不是美滋滋?错!

因此前面的 ④ 优化方案 也不是一个最完美的解决方案.那么,这也不行,那也不行,那该怎么办?放心,苹果爸爸已经给我们准备好后路了!

五、消息转发机制

在慢速查找的流程(lookUpImpOrForward)中,我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发,但是,我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法

instrumentObjcMessageSends

通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现,所以,在main中调用instrumentObjcMessageSends打印方法调用的日志信息,有以下两点准备工作

快速转发流程

forwardingTargetForSelector在源码中只有一个声明,并没有其它描述,好在帮助文档中提到了关于它的解释:

快速转发流程解决崩溃

如下代码就是通过快速转发解决崩溃——即TCJPerson实现不了的方法,转发给TCJStudent去实现(转发给已经实现该方法的对象)

也可以直接不指定消息接收者,直接调用父类的该方法,如果还是没有找到,则直接报错

慢速转发流程

在快速转发流程找不到转发的对象后,会来到慢速转发流程methodSignatureForSelector
依葫芦画瓢,在帮助文档中找到methodSignatureForSelector

点击查看forwardInvocation

慢速转发流程解决崩溃

慢速转发流程就是先methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过对NSInvocation来实现消息的转发

其实也可以对forwardInvocation方法中的invocation不进行处理,也不会崩溃报错

所以,由上述可知,无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃.

通过hopper/IDA反汇编消息转发机制

Hopper和IDA是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图等,下面以Hopper为例.

通过上面两种查找方式可以验证,消息转发的方法有3个

消息转发整体的流程如下!](https://img.haomeiwen.com/i2340353/0630f3b4f1f7b6ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

消息转发的处理主要分为两部分:

六、动态方法决议为什么执行两次?

在前文中提及了动态方法决议方法执行了两次,有以下两种分析方式

启用上帝视角的探索

在慢速查找流程中,我们了解到resolveInstanceMethod方法的执行是通过lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod来到resolveInstanceMethod源码,在源码中通过发送resolve_sel消息触发,如下所示

所以可以在resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);处加一个断点,通过bt打印堆栈信息来看到底发生了什么

这一点可以通过代码调试来验证,如下所示,在class_getInstanceMethod方法处加一个断点,在执行了methodSignatureForSelector方法后,返回了签名,说明方法签名是生效的,苹果在走到invocation之前,给了开发者一次机会再去查询,所以走到class_getInstanceMethod这里,又去走了一遍方法查询say666,然后会再次走到动态方法决议

所以,上述的分析也印证了前文中resolveInstanceMethod方法执行了两次的原因

无上帝视角的探索

如果在没有上帝视角的情况下,我们也可以通过代码来推导在哪里再次调用了动态方法决议

通过运行发现,如果赋值了IMP,动态方法决议只会走一次,说明不是在这里走第二次动态方法决议

继续往下探索

结果发现resolveInstanceMethod中的打印还是只打印了一次,数排名第二次动态方法决议 在forwardingTargetForSelector方法后

结果发现第二次动态方法决议在 methodSignatureForSelectorforwardInvocation方法之间.

第二种分析同样可以论证前文中resolveInstanceMethod执行了两次的原因.
经过上面的论证,我们了解到其实在慢速消息转发流程中,在methodSignatureForSelectorforwardInvocation方法之间还有一次动态方法决议,即苹果再次给的一个机会,如下图所示

写在后面

到目前为止,objc_msgSend发送消息的流程就分析完成了,在这里简单总结下

最后,和谐学习,不急不躁.我还是我,颜色不一样的烟火.

上一篇 下一篇

猜你喜欢

热点阅读