美文网首页iOS Runtime必看faceruntime.runloop
iOS之武功秘籍⑥:Runtime之方法与消息

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

作者: 長茳 | 来源:发表于2021-02-22 23:39 被阅读0次

    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使用

    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

    ② 代码转换

    • ((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"))

    这其中需要注意两点:

    • 1、直接调用objc_msgSend,需要导入头文件#import <objc/message.h>
    • 2、需要将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——父类元类对象

    三、消息查找流程

    消息查找流程其实是通过上层的方法编号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,此时并不是汇编实现.

    注意

    • 1、C/C++中调用 汇编 ,去查找汇编时C/C++调用的方法需要多加一个下划线
    • 2、汇编 中调用 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 ,运行程序报错,提示方法未实现,如下所示

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

    四、动态方法解析

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

    主要分为以下几步

    • 判断类是否是元类
      • 如果是,执行实例方法的动态方法决议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封装的时候需要给一个容错空间

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

    五、消息转发机制

    在慢速查找的流程(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 目录,如下所示

    • 运行代码,并前往/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

    消息转发整体的流程如下!](https://img.haomeiwen.com/i2340353/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消息触发,如下所示

    所以可以在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源码中搜索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

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

    相关文章

      网友评论

        本文标题:iOS之武功秘籍⑥:Runtime之方法与消息

        本文链接:https://www.haomeiwen.com/subject/horlfltx.html