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

iOS之武功秘籍 文章汇总

写在前面

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

本节可能用到的秘籍Demo

一、Runtime

① 什么是Runtime?

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

  • 运行时:代码跑起来,将可执行文件装载到内存
  • 编译时:正在编译的时间——翻译源代码将高级语言(OC、Swift)翻译成机器语言(汇编等),最后变成二进制

② Runtime版本

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

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

③ Runtime的作用及调用

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

iOS之武功秘籍⑥:Runtime之方法与消息_第1张图片

Runtime调用方式:

  • Runtime API,如 sel_registerName(),class_getInstanceSize
  • NSObject API,如 isKindOf()
  • OC上层方式,如 @selector()

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

二、方法的本质

① 研究方法

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

  • 兼容编译(代码少):clang -rewrite-objc main.m -o main.cpp
  • 完整编译(不报错):xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cppxcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

② 代码转换

iOS之武功秘籍⑥:Runtime之方法与消息_第2张图片
iOS之武功秘籍⑥:Runtime之方法与消息_第3张图片
  • ((TCJPerson *(*)(id, SEL))(void *)是类型强转
  • (id)objc_getClass("TCJPerson")获取TCJPerson类对象
  • sel_registerName("alloc")等同于@selector()

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

③ 方法的本质

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

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

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

iOS之武功秘籍⑥:Runtime之方法与消息_第4张图片

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

这其中需要注意两点:

  • 1、直接调用objc_msgSend,需要导入头文件#import
  • 2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
    iOS之武功秘籍⑥:Runtime之方法与消息_第5张图片

④ 向不同对象发送消息

iOS之武功秘籍⑥:Runtime之方法与消息_第6张图片

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

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

① 发送实例方法

消息接收者——实例对象

iOS之武功秘籍⑥:Runtime之方法与消息_第7张图片

② 发送类方法

iOS之武功秘籍⑥:Runtime之方法与消息_第8张图片

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

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

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

iOS之武功秘籍⑥:Runtime之方法与消息_第9张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第10张图片

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

④ 向父类发送实例方法

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

iOS之武功秘籍⑥:Runtime之方法与消息_第11张图片

⑤ 向父类发送类方法

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

iOS之武功秘籍⑥:Runtime之方法与消息_第12张图片

三、消息查找流程

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

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

  • C语言不能通过写一个函数,保留未知的参数,跳转到任意的指针,而汇编有寄存器
  • 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别

① 开始查找

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

iOS之武功秘籍⑥:Runtime之方法与消息_第13张图片

iOS之武功秘籍⑥:Runtime之方法与消息_第14张图片

iOS之武功秘籍⑥:Runtime之方法与消息_第15张图片

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

  • ①开始objc_msgSend
  • ②判断消息接收者是否为空,为空直接返回
  • ③判断tagged_pointers(之后会讲到)
  • ④取得对象中的isa存一份到p13
  • ⑤根据isa进行mask地址偏移得到对应的上级对象(类、元类)
    iOS之武功秘籍⑥:Runtime之方法与消息_第16张图片

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

  • ⑥开始在缓存中查找imp——开始了快速流程

② 快速查找流程

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

iOS之武功秘籍⑥:Runtime之方法与消息_第17张图片

  • ①通过cache首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址 占8字节,superClass8字节),获取cahcecache中高16位存mask,低48位存buckets,即p11 = cache
  • ②从cache中分别取出bucketsmask,并由mask根据哈希算法计算出哈希下标
    • 通过cache掩码(即0x0000ffffffffffff)& 运算,将高16位mask抹零,得到buckets指针地址,即p10 = buckets
    • cache右移48位,得到mask,即p11 = mask
    • objc_msgSend的参数p1(即第二个参数_cmd)& msak,通过哈希算法,得到需要查找存储sel-impbucket下标index,即p12 = index = _cmd & mask,为什么通过这种方式呢?因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示
      iOS之武功秘籍⑥:Runtime之方法与消息_第18张图片
  • ③根据所得的哈希下标indexbuckets首地址,取出哈希下标对应的bucket
    • 其中PTRSHIFT等于3,左移4位(即2^4 = 16字节)的目的是计算出一个bucket实际占用的大小,结构体bucket_tsel8字节,imp8字节
    • 根据计算的哈希下标index 乘以 单个bucket占用的内存大小,得到buckets首地址在实际内存中的偏移量
    • 通过首地址 + 实际偏移量,获取哈希下标index对应的bucket
  • ④根据获取的bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel
  • ⑤第一次递归循环
    • 比较获取的bucketselobjc_msgSend的第二个参数的_cmd(即p1)是否相等
    • 如果相等,则直接跳转至CacheHit,即缓存命中,返回imp
    • 如果不相等,有以下两种情况
      • 如果一直都找不到,直接跳转至CheckMiss,因为$0normal,会跳转至__objc_msgSend_uncached,即进入慢速查找流程
      • 如果根据index获取的bucket 等于 buckets的第一个元素,则人为的将当前bucket设置为buckets的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环),即⑥
      • 如果当前bucket不等于buckets的第一个元素,则继续向前查找,进入第一次递归循环
  • ⑥第二次递归循环:重复⑤的操作,与⑤中唯一区别是,如果当前的bucket还是等于 buckets的第一个元素,则直接跳转至JumpMiss,此时的$0normal,也是直接跳转至__objc_msgSend_uncached,即进入慢速查找流程

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

iOS之武功秘籍⑥:Runtime之方法与消息_第19张图片

③ 慢速查找流程

① 慢速查找-汇编部分

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

  • objc-msg-arm64.s文件中查找__objc_msgSend_uncached的汇编实现,其中的核心是MethodTableLookup(即查询方法列表),其源码如下
    iOS之武功秘籍⑥:Runtime之方法与消息_第20张图片
  • 搜索MethodTableLookup的汇编实现,其中的核心是_lookUpImpOrForward,汇编源码实现如下
    iOS之武功秘籍⑥:Runtime之方法与消息_第21张图片

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

  • main中,例如[person sayHello]对象方法调用处加一个断点,并且开启汇编调试【Debug -- Debug worlflow -- 勾选Always show Disassembly】,运行程序

    iOS之武功秘籍⑥:Runtime之方法与消息_第22张图片

  • 汇编中objc_msgSend加一个断点,执行断住,按住control + stepinto,进入objc_msgSend的汇编

    iOS之武功秘籍⑥:Runtime之方法与消息_第23张图片

  • _objc_msgSend_uncached加一个断点,执行断住,按住control + stepinto,进入汇编

    iOS之武功秘籍⑥:Runtime之方法与消息_第24张图片

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

注意

  • 1、C/C++中调用 汇编 ,去查找汇编时C/C++调用的方法需要多加一个下划线
  • 2、汇编 中调用 C/C++方法时,去查找C/C++方法,需要将汇编调用的方法去掉一个下划线

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

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

其整体的慢速查找流程如图所示
iOS之武功秘籍⑥:Runtime之方法与消息_第25张图片

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

  • cache缓存中进行查找,即快速查找,找到则直接返回imp,反之,则进入②
  • ②判断cls
    • 是否是已知类,如果不是,则报错
    • 类是否实现,如果没有,则需要先实现,确定其父类链,此时实例化的目的是为了确定父类链、ro、以及rw等,方便后续数据的读取以及查找的循环
    • 是否初始化,如果没有,则初始化
  • for循环,按照类继承链 或者 元类继承链的顺序查找
    • 当前cls的方法列表中使用二分查找算法查找方法,如果找到,则进入cache写入流程(在iOS之武功秘籍⑤:cache_t分析文章中已经详述过),并返回imp,如果没有找到,则返回nil
    • 当前cls被赋值为父类,如果父类等于nil,则imp = 消息转发,并终止递归,进入④
    • 如果父类链中存在循环,则报错,终止循环
    • 父类缓存中查找方法
      • 如果未找到,则直接返回nil,继续循环查找
      • 如果找到,则直接返回imp,执行cache写入流程
  • 判断是否执行过动态方法解析
    • 如果没有,执行动态方法解析
    • 如果执行过一次动态方法解析,则走到消息转发流程

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

getMethodNoSuper_nolock方法:二分查找方法列表

查找方法列表的流程如下所示

其二分查找核心的源码实现如下
iOS之武功秘籍⑥:Runtime之方法与消息_第26张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第27张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第28张图片

cache_getImp方法:父类缓存查找

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

iOS之武功秘籍⑥:Runtime之方法与消息_第29张图片

  • 如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,则直接返回imp
  • 如果在父类缓存中,没有找到方法实现,则跳转至CheckMiss 或者 JumpMiss,通过判断$0 跳转至LGetImpMiss,直接返回nil.

总结

  • 对于对象方法(即实例方法),即在类中查找,其慢速查找的父类链是:类--父类--根类--nil
  • 对于类方法,即在元类中查找,其慢速查找的父类链是:元类--根元类--根类--nil
  • 如果快速查找、慢速查找也没有找到方法实现,则尝试动态方法决议
  • 如果动态方法决议仍然没有找到,则进行消息转发
常见方法未实现报错源码

如果在快速查找、慢速查找、方法解析流程中,均没有找到实现,则使用消息转发,其流程如下
iOS之武功秘籍⑥:Runtime之方法与消息_第30张图片

消息转发会实现

  • 其中_objc_msgForward_impcache是汇编实现,会跳转至__objc_msgForward,其核心是__objc_forward_handler
    iOS之武功秘籍⑥:Runtime之方法与消息_第31张图片
  • 汇编实现中查找__objc_forward_handler,并没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler,有如下实现,本质是调用的objc_defaultForwardHandler方法
    iOS之武功秘籍⑥:Runtime之方法与消息_第32张图片

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

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

iOS之武功秘籍⑥:Runtime之方法与消息_第33张图片
iOS之武功秘籍⑥:Runtime之方法与消息_第34张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第35张图片
iOS之武功秘籍⑥:Runtime之方法与消息_第36张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第37张图片

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

四、动态方法解析

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

iOS之武功秘籍⑥:Runtime之方法与消息_第38张图片

主要分为以下几步

  • 判断类是否是元类
    • 如果是,执行实例方法的动态方法决议resolveInstanceMethod
    • 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为,则在元类实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

其流程如下
iOS之武功秘籍⑥:Runtime之方法与消息_第39张图片

① 实例方法

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

iOS之武功秘籍⑥:Runtime之方法与消息_第40张图片

主要分为以下几个步骤:

  • 在发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 如果没有,则直接返回
    • 如果有,则发送resolveInstanceMethod消息
  • 再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法

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

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

iOS之武功秘籍⑥:Runtime之方法与消息_第41张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第42张图片

③ 类方法

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

iOS之武功秘籍⑥:Runtime之方法与消息_第43张图片

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

④ 优化方案

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

  • 实例方法:类 -- 父类 -- 根类 -- nil
  • 类方法:元类 -- 根元类 -- 根类 -- nil

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

iOS之武功秘籍⑥:Runtime之方法与消息_第44张图片

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

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

⑤ 动态方法决议总结

  • 实例方法可以重写resolveInstanceMethod添加imp
  • 类方法可以在本类重写resolveClassMethod元类添加imp,或者在NSObject分类重写resolveInstanceMethod添加imp
  • 动态方法解析只要在任意一步lookUpImpOrNil查找到imp就不会查找下去——即本类做了动态方法决议,不会走到NSObjct分类的动态方法决议
  • 所有方法都可以通过在NSObject分类重写resolveInstanceMethod添加imp解决崩溃

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

  • 统一处理起来耦合度高
  • 逻辑判断多
  • 可能在NSObjct分类动态方法决议之前已经做了处理
  • SDK封装的时候需要给一个容错空间

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

五、消息转发机制

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

  • 通过instrumentObjcMessageSends方式打印发送消息的日志

instrumentObjcMessageSends

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

  • 1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES

  • 2、在main中通过extern 声明instrumentObjcMessageSends方法[图片上传失败...(image-1b897a-1614008219381)]

  • 通过logMessageSend源码,了解到消息发送打印信息存储在/tmp/msgSends 目录,如下所示

    iOS之武功秘籍⑥:Runtime之方法与消息_第45张图片

  • 运行代码,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法
    • 两次动态方法决议:resolveInstanceMethod方法
    • 两次消息快速转发:forwardingTargetForSelector方法
    • 两次消息慢速转发:methodSignatureForSelector + resolveInvocation
      iOS之武功秘籍⑥:Runtime之方法与消息_第46张图片

快速转发流程

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

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
  • 该方法的效率较高,如果不实现,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数、返回值等应和原方法相同
    iOS之武功秘籍⑥:Runtime之方法与消息_第47张图片

快速转发流程解决崩溃

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

iOS之武功秘籍⑥:Runtime之方法与消息_第48张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第49张图片

慢速转发流程

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

iOS之武功秘籍⑥:Runtime之方法与消息_第50张图片

点击查看forwardInvocation

  • forwardInvocationmethodSignatureForSelector必须是同时存在的,底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用
  • 查找可以响应NSInvocation中编码的消息的对象(对于所有消息,此对象不必相同)
  • 使用anInvocation将消息发送到该对象.anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者
    iOS之武功秘籍⑥:Runtime之方法与消息_第51张图片

慢速转发流程解决崩溃

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

iOS之武功秘籍⑥:Runtime之方法与消息_第52张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第53张图片

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

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

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

  • 运行程序崩溃,查看堆栈信息
    iOS之武功秘籍⑥:Runtime之方法与消息_第54张图片
  • 发现___forwarding___来自CoreFoundation
    iOS之武功秘籍⑥:Runtime之方法与消息_第55张图片
  • 通过image list,读取整个镜像文件,然后搜索CoreFoundation,查看其可执行文件的路径
    iOS之武功秘籍⑥:Runtime之方法与消息_第56张图片
  • 通过文件路径,找到CoreFoundation的可执行文件
    iOS之武功秘籍⑥:Runtime之方法与消息_第57张图片
  • 打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)
    iOS之武功秘籍⑥:Runtime之方法与消息_第58张图片
    iOS之武功秘籍⑥:Runtime之方法与消息_第59张图片
  • 以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码
    iOS之武功秘籍⑥:Runtime之方法与消息_第60张图片
  • 通过左侧的搜索框搜索__forwarding_prep_0___,然后选择伪代码

  • 以下是__forwarding_prep_0___的汇编伪代码,跳转至___forwarding___

    iOS之武功秘籍⑥:Runtime之方法与消息_第61张图片

  • 以下是___forwarding___的伪代码实现,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_6459b即快速转发没有响应,进入慢速转发流程

    iOS之武功秘籍⑥:Runtime之方法与消息_第62张图片

  • 跳转至loc_6459b,在其下方判断是否响应methodSignatureForSelector方法

    iOS之武功秘籍⑥:Runtime之方法与消息_第63张图片

  • 如果没有响应,跳转至loc_6490b,则直接报错

  • 如果获取methodSignatureForSelector的方法签名为nil,也是直接报错

    iOS之武功秘籍⑥:Runtime之方法与消息_第64张图片

  • 如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理

    iOS之武功秘籍⑥:Runtime之方法与消息_第65张图片

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

  • 【快速转发】forwardingTargetForSelector
  • 【慢速转发】
    • methodSignatureForSelector
    • forwardInvocation

消息转发整体的流程如下!](https://upload-images.jianshu.io/upload_images/2340353-0630f3b4f1f7b6ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

  • 【快速转发】当慢速查找,以及动态方法决议均没有找到实现时,进行消息转发,首先是进行快速消息转发,即走到forwardingTargetForSelector方法
    • 如果返回消息接收者,在消息接收者中还是没有找到方法实现,则进入另一个方法的查找流程
    • 如果返回nil,则进入慢速消息转发
  • 【慢速转发】执行到methodSignatureForSelector方法
    • 如果返回的方法签名nil,则直接崩溃报错
    • 如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错

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

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

启用上帝视角的探索

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

iOS之武功秘籍⑥:Runtime之方法与消息_第66张图片

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

  • resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);处加一个断点,运行程序,直到第一次“来了”,通过bt查看第一次动态方法决议的堆栈信息,此时的selsay666
    iOS之武功秘籍⑥:Runtime之方法与消息_第67张图片
  • 继续往下执行,直到第二次“来了”打印,查看堆栈信息,在第二次中,我们可以看到是通过CoreFoundation-[NSObject(NSObject) methodSignatureForSelector:]方法,然后通过class_getInstanceMethod再次进入动态方法决议
    iOS之武功秘籍⑥:Runtime之方法与消息_第68张图片
  • 通过上一步的堆栈信息,我们需要去看看CoreFoundation中到底做了什么?通过Hopper反汇编CoreFoundation的可执行文件,查看methodSignatureForSelector方法的伪代码
    iOS之武功秘籍⑥:Runtime之方法与消息_第69张图片
  • 通过methodSignatureForSelector伪代码进入___methodDescriptionForSelector的实现
    iOS之武功秘籍⑥:Runtime之方法与消息_第70张图片
  • 进入 ___methodDescriptionForSelector的伪代码实现,结合汇编的堆栈打印,可以看到,在___methodDescriptionForSelector这个方法中调用了objc4源码class_getInstanceMethod
    iOS之武功秘籍⑥:Runtime之方法与消息_第71张图片
  • objc4源码中搜索class_getInstanceMethod,其源码实现如下所示
    iOS之武功秘籍⑥:Runtime之方法与消息_第72张图片

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

iOS之武功秘籍⑥:Runtime之方法与消息_第73张图片

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

无上帝视角的探索

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

  • TCJPerson类中重写resolveInstanceMethod方法,并加上class_addMethod操作即赋值IMP,此时resolveInstanceMethod会走两次吗?
    iOS之武功秘籍⑥:Runtime之方法与消息_第74张图片

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

继续往下探索

  • 去掉resolveInstanceMethod方法中的赋值IMP,在TCJPerson类中重写forwardingTargetForSelector方法,并指定返回值为[TCJStudent alloc],重新运行,如果resolveInstanceMethod打印了两次,说明是在forwardingTargetForSelector方法之前执行了动态方法决议,反之,在forwardingTargetForSelector方法之后
    iOS之武功秘籍⑥:Runtime之方法与消息_第75张图片

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

  • TCJPerson类中重写 methodSignatureForSelectorforwardInvocation,运行
    iOS之武功秘籍⑥:Runtime之方法与消息_第76张图片

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

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

iOS之武功秘籍⑥:Runtime之方法与消息_第77张图片

写在后面

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

  • 【快速查找流程】首先,在类的缓存cache中查找指定方法的实现
  • 【慢速查找流程】如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
  • 【动态方法决议】如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
  • 【消息转发】如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
  • 如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance

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

你可能感兴趣的:(iOS之武功秘籍⑥:Runtime之方法与消息)