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

iOS之武功秘籍 文章汇总html

写在前面

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

本节可能用到的秘籍Demogit

1、Runtime

① 什么是Runtime?

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

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

② Runtime版本

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

源码中-old__OBJC__表明Legacy版本,-new__OBJC2__表明Modern版本,以此作兼容缓存

③ Runtime的做用及调用

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

Runtime调用方式:markdown

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

原来日常在用的这么多方法都是Runtime啊,那么方法到底是什么呢?app

2、方法的本质

① 研究方法

经过clang编译成cpp文件能够看到底层代码,获得方法的本质iphone

  • 兼容编译(代码少):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

② 代码转换

  • ((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]的调用,查看其打印是不是一致. 其打印结果以下,发现是一致的,因此 [person sayHello]等价于objc_msgSend(person,sel_registerName("sayHello"))

这其中须要注意两点:

  • 一、直接调用objc_msgSend,须要导入头文件#import <objc/message.h>
  • 二、须要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改成NO,将严厉的检查机制关掉,不然objc_msgSend的参数会报错

④ 向不一样对象发送消息

子类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——父类元类对象

3、消息查找流程

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

objc_msgSend是用汇编写成的,至于为何不用C而用汇编写,是由于:

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

① 开始查找

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

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

  • ①开始objc_msgSend
  • ②判断消息接收者是否为空,为空直接返回
  • ③判断tagged_pointers(以后会讲到)
  • ④取得对象中的isa存一份到p13
  • ⑤根据isa进行mask地址偏移获得对应的上级对象(类、元类)

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

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

② 快速查找流程

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

  • ①经过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时,也是经过一样哈希算法计算哈希下标进行存储,因此读取也须要经过一样的方式读取,以下所示
  • ③根据所得的哈希下标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,即进入慢速查找流程

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

③ 慢速查找流程

① 慢速查找-汇编部分

在快速查找流程中,若是没有找到方法实现,不管是走到CheckMiss仍是JumpMiss,最终都会走到__objc_msgSend_uncached汇编函数

  • objc-msg-arm64.s文件中查找__objc_msgSend_uncached的汇编实现,其中的核心是MethodTableLookup(即查询方法列表),其源码以下

  • 搜索MethodTableLookup的汇编实现,其中的核心是_lookUpImpOrForward,汇编源码实现以下

验证 上述汇编的过程,能够经过汇编调试来验证

  • main中,例如[person sayHello]对象方法调用处加一个断点,而且开启汇编调试【Debug -- Debug worlflow -- 勾选Always show Disassembly】,运行程序
  • 汇编中objc_msgSend加一个断点,执行断住,按住control + stepinto,进入objc_msgSend的汇编
  • _objc_msgSend_uncached加一个断点,执行断住,按住control + stepinto,进入汇编

从上能够看出最后走到的就是lookUpImpOrForward,此时并非汇编实现.

注意

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

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

根据汇编部分的提示,全局续搜索lookUpImpOrForward,最后在objc-runtime-new.mm文件中找到了源码实现,这是一个c实现的函数 其总体的慢速查找流程如图所示 慢速流程主要分为几个步骤:

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

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

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

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

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

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

cache_getImp方法:父类缓存查找

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

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

总结

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

若是在快速查找、慢速查找、方法解析流程中,均没有找到实现,则使用消息转发,其流程以下

消息转发会实现

  • 其中_objc_msgForward_impcache是汇编实现,会跳转至__objc_msgForward,其核心是__objc_forward_handler

  • 汇编实现中查找__objc_forward_handler,并无找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler,有以下实现,本质是调用的objc_defaultForwardHandler方法

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

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

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

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

下面,咱们来说讲如何在崩溃前,如何操做,能够防止方法未实现的崩溃.

4、动态方法解析

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

  • 判断类是不是元类
    • 若是是,执行实例方法的动态方法决议resolveInstanceMethod
    • 若是是元类,执行类方法的动态方法决议resolveClassMethod,若是在元类中没有找到或者为,则在元类实例方法的动态方法决议resolveInstanceMethod中查找,主要是由于类方法在元类中是实例方法,因此还须要查找元类中实例方法的动态方法决议
  • 若是动态方法决议中,将其实现指向了其余方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程

其流程以下

① 实例方法

针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,咱们有一次挽救的机会,即尝试一次动态方法决议,因为是实例方法,因此会走到resolveInstanceMethod方法,其源码以下 主要分为如下几个步骤:

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

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

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

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

③ 类方法

针对类方法,与实例方法相似,一样能够经过重写resolveClassMethod类方法来解决前文的崩溃问题,即在TCJPerson类中重写该方法,并将sayNB类方法的实现指向类方法sayHappy resolveClassMethod类方法的重写须要注意一点,传入的cls再也不是类而是元类,能够经过objc_getMetaClass方法获取类的元类,缘由是由于类方法在元类中是实例方法.

④ 优化方案

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

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

它们的共同点是若是前面没找到,都会来到根类即NSObject中查找,因此咱们是否能够将上述的两个方法统一整合在一块儿呢?答案是能够的,能够经过NSObject添加分类的方式来实现统一处理,并且因为类方法的查找,在其继承链,查找的也是实例方法,因此能够将实例方法 和 类方法的统一处理放在resolveInstanceMethod方法中,以下所示 这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为何调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本缘由仍是类方法在元类中是实例方法.

固然,上面这种写法仍是会有其余的问题,好比系统方法也会被更改,针对这一点,是能够优化的,即咱们能够针对自定义类中方法统一方法名的前缀,根据前缀来判断是不是自定义方法,而后统一处理自定义方法,例如能够在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提高用户的体验.

⑤ 动态方法决议总结

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

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

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

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

5、消息转发机制

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

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

instrumentObjcMessageSends

经过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现,因此,在main中调用instrumentObjcMessageSends打印方法调用的日志信息,有如下两点准备工做

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

  • 二、在main中经过extern 声明instrumentObjcMessageSends方法

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

  • 运行代码,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件,打开发如今崩溃前,执行了如下方法

    • 两次动态方法决议:resolveInstanceMethod方法
    • 两次消息快速转发:forwardingTargetForSelector方法
    • 两次消息慢速转发:methodSignatureForSelector + resolveInvocation

快速转发流程

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

  • 该方法的返回对象是执行sel的新对象,也就是本身处理不了会将消息转发给别的对象进行相关方法的处理,可是不能返回self,不然会一直找不到
  • 该方法的效率较高,若是不实现,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数、返回值等应和原方法相同

快速转发流程解决崩溃

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

也能够直接不指定消息接收者,直接调用父类的该方法,若是仍是没有找到,则直接报错

慢速转发流程

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

  • forwardInvocationmethodSignatureForSelector必须是同时存在的,底层会经过方法签名,生成一个NSInvocation,将其做为参数传递调用
  • 查找能够响应NSInvocation中编码的消息的对象(对于全部消息,此对象没必要相同)
  • 使用anInvocation将消息发送到该对象.anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者

慢速转发流程解决崩溃

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

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

因此,由上述可知,不管在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃.

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

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

  • 运行程序崩溃,查看堆栈信息

  • 发现___forwarding___来自CoreFoundation

  • 经过image list,读取整个镜像文件,而后搜索CoreFoundation,查看其可执行文件的路径

  • 经过文件路径,找到CoreFoundation的可执行文件

  • 打开hopper,选择Try the Demo,而后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)

  • 如下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码

  • 经过左侧的搜索框搜索__forwarding_prep_0___,而后选择伪代码

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

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

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

  • 若是没有响应,跳转至loc_6490b,则直接报错

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

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

经过上面两种查找方式能够验证,消息转发的方法有3个

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

消息转发总体的流程以下

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

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

6、动态方法决议为何执行两次?

在前文中说起了动态方法决议方法执行了两次,有如下两种分析方式

启用上帝视角的探索

在慢速查找流程中,咱们了解到resolveInstanceMethod方法的执行是经过lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod来到resolveInstanceMethod源码,在源码中经过发送resolve_sel消息触发,以下所示 因此能够在resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);处加一个断点,经过bt打印堆栈信息来看到底发生了什么

  • resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);处加一个断点,运行程序,直到第一次“来了”,经过bt查看第一次动态方法决议的堆栈信息,此时的selsay666

  • 继续往下执行,直到第二次“来了”打印,查看堆栈信息,在第二次中,咱们能够看到是经过CoreFoundation-[NSObject(NSObject) methodSignatureForSelector:]方法,而后经过class_getInstanceMethod再次进入动态方法决议

  • 经过上一步的堆栈信息,咱们须要去看看CoreFoundation中到底作了什么?经过Hopper反汇编CoreFoundation的可执行文件,查看methodSignatureForSelector方法的伪代码

  • 经过methodSignatureForSelector伪代码进入___methodDescriptionForSelector的实现

  • 进入 ___methodDescriptionForSelector的伪代码实现,结合汇编的堆栈打印,能够看到,在___methodDescriptionForSelector这个方法中调用了objc4源码class_getInstanceMethod

  • objc4-818.2源码中搜索class_getInstanceMethod,其源码实现以下所示

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

因此,上述的分析也印证了前文中resolveInstanceMethod方法执行了两次的缘由

无上帝视角的探索

若是在没有上帝视角的状况下,咱们也能够经过代码来推导在哪里再次调用了动态方法决议

  • TCJPerson类中重写resolveInstanceMethod方法,并加上class_addMethod操做即赋值IMP,此时resolveInstanceMethod会走两次吗?

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

继续往下探索

  • 去掉resolveInstanceMethod方法中的赋值IMP,在TCJPerson类中重写forwardingTargetForSelector方法,并指定返回值为[TCJStudent alloc],从新运行,若是resolveInstanceMethod打印了两次,说明是在forwardingTargetForSelector方法以前执行了动态方法决议,反之,在forwardingTargetForSelector方法以后

结果发现resolveInstanceMethod中的打印仍是只打印了一次,那说明第二次动态方法决议 在forwardingTargetForSelector方法后

  • TCJPerson类中重写 methodSignatureForSelectorforwardInvocation,运行

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

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

写在后面

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

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

最后,和谐学习,不急不躁.我仍是我,颜色不同的烟火.

相关文章
相关标签/搜索