03-OC方法调用的底层分析

OC底层原理探索文档汇总

分析对象进行方法调用在底层的执行过程,包括快速查找流程、慢速查找流程、动态方法解析、消息转发,以及最后查询失败的报错方法。

主要内容:

  1. 快速查找流程
  2. 慢速查找流程
  3. 动态方法解析
  4. 消息转发(消息接收者重定向,消息重定向)
  5. 查询失败提示

1、前期准备

1.1 Runtime的简单认识

Runtime和方法调用的基本认识可以看我的另一篇博客Runtime官方指导文档阅读

1.1.1 Runtime是什么

OC具有面向对象的特性和动态机制。OC把一些决定性的工作从编译时、链接时推迟到运行时。而Runtime给我们提供了这个运行时,我们可以用runtime在运行时动态的创建修改类、对象、方法、属性、协议等操作

编译时是源代码翻译成机器能识别的代码的过程,不同于编译时,运行时是代码跑起来,被装载到内存中的过程,这是一个动态的过程。

1.1.2 Runtime的作用是什么

  • 为面向对象提供运行时环境
  • 进行内存布局和底层执行逻辑

1.2 isa、cache、方法列表的简单认识

方法查找的过程涉及到了isa、cache、方法列表的认识,这需要了解对象的底层和类的底层,详情可以看我的另一篇博客OC类的底层分析

简单认识:

isa是对象中的一个属性,它包含有类信息,因此我们可以通过对象的isa来获取到类,再通过类获取类中的数据,比如方法、属性、协议、成员变量。

cache是类中的一个成员,它包含有sel和imp,当我们想要通过sel查询imp时,会先来到该类的cache中查找。

方法列表在类的rw中,当我们想要通过sel查询imp时,就需要在方法列表中查询。

1.3 如何探索呢?

之前在看博客时,很多博客对于一些知识的说明都是凭空而来,比如objc_msgSend的实现先进行cache查找后进行方法列表的查找,方法查找结束后会进行动态方法解析只说是这样的,但是为什么是这样的并没有说。

\color{red}{ 因此我这里对于方法调用的分析每一步都是有迹可循的,重要的是探索,而不是知识点记忆。 }

  1. 先从方法上层调用入手,通过Clang查看底层是如何实现的。经查看发现是objc_msgSend来实现的
  2. 通过objc_msgSend在源码中查看,在汇编中找到了objc_msgSend的实现过程,探索汇编发现是进行cache的查找。
  3. cache查找结束后会进入到一个lookUpImpOrForward方法,之后在源码C语言中查找到lookUpImpOrForward的实现,探索发现其内部是在进行方法列表的查找。
  4. 方法的查找后如果没有查找到,发现会进入到一个resolveMethod_locked方法,由此开启动态方法解析的过程。
  5. 方法查找失败后,会得到一个报错函数_objc_msgForward_impcache,继续探索发现就是我们常见的方法调用失败后的报错信息。
  6. 在动态方法解析后会再次进行方法列表的查找,下一步找不到了消息转发,因此通过instrumentObjcMessageSends方式打印发送消息的日志和通过hopper/IDA反编译两种方式探索消息转发的过程。

2、方法的本质

既然要探索方法的调用,首先需要知道方法是什么。这里使用Clang就可以清楚的看到方法调用在底层其实是通过objc_msgSend进行消息发送。

\color{red}{ 一句话:方法的本质就是消息发送 }

发送消息就是:给一个接受者对象发送一个消息,告诉接受者对象我们要执行哪个函数。

消息函数有多种,我们只分析常见的两种objc_msgSend和objc_msgSendSuper。

2.1 objc_msgSend的认识

2.1.1 底层结构

源码:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

说明:

  • self表示当前对象,通过对象获取所在类的方法列表
  • op是方法选择器,通过选择器sel来查找imp
  • self和op是必须有的隐藏参数,如果方法有其他参数,还会有其他参数。

2.1.2 验证

代码:

#import "WYPerson.h"
#import "WYCat.h"
#import "objc/runtime.h"
#import "objc/message.h"

@implementation WYPerson

- (void)msgSendTest:(BOOL)abc{
    NSLog(@"测试objc_msgSend");
}

注意:

  • 想要调用objc_msgSend,必须导入头文件#import "objc/message.h"
  • 需要将target --> Build Setting -->搜索msg -- 将enable strict checking of calls由YES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错

方法调用

//方法调用
WYPerson *person = [WYPerson alloc];
objc_msgSend(person,sel_registerName("msgSendTest:"),YES);

//结果:
2021-10-15 19:16:19.021944+0800 消息发送[2866:47753] 测试objc_msgSend

2.2 objc_msgSendSuper的认识

2.2.1 底层结构

源码:

//结构体    
struct objc_super {
    /// Specifies an instance of a class.类的实例
    __unsafe_unretained _Nonnull id receiver;//接受者

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;//父类
#endif
    /* super_class is the first class to search */
}; 

//函数
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);  

说明:

  • super是一个objc_super结构体
  • op是方法选择器
  • objc_super结构体中包含类的接受者和父类

2.2.2 使用验证

代码:

父类WYPerson
实现msgSendSuperTest方法

@interface WYPerson : NSObject
- (void) msgSendSuperTest;
@end

@implementation WYPerson

- (void) msgSendSuperTest {
    NSLog(@"%s: 测试objc_msgSendSuper",__func__);
}
@end

子类WYStudent
没有实现msgSendSuperTest方法

@interface WYStudent : NSObject

- (void) msgSendSuperTest;
@end

#import "WYStudent.h"

@implementation WYStudent

@end

main函数调用

WYPerson *person = [WYPerson alloc];
WYStudent *student = [WYStudent alloc];
    
struct objc_super wySuper;
wySuper.receiver = person;
wySuper.super_class = [WYStudent class];
    
objc_msgSendSuper(&wySuper, sel_registerName("msgSendSuperTest"));

结果:

2021-10-15 20:14:45.809788+0800 消息发送[4750:91856] -[WYPerson msgSendSuperTest]: 测试objc_msgSendSuper
2021-10-15 20:14:45.809861+0800 消息发送[4750:91856] -[WYPerson msgSendSuperTest]: 测试objc_msgSendSuper

说明:

  • 当通过子类调用父类的方法时在底层会使用objc_msgSendSuper来获取父类的方法
  • 只是我们在上层没有感知
  • 在objc_super结构体中设置接收者为当前对象,再设置它的父类。注意此时接受者仍然是当前对象,而不是父类

2.2.3 案例分析

WYStudent继承自WYPerson,WYStudent创建一个方法objc_msgSendSuperTest,大家觉得调用这个方法会打印什么呢?

- (void)objc_msgSendSuperTest{
    NSLog(@"父类:%@---子类:%@",[super class],[self class]);
}

分析:
均打印WYStudent,这是因为调用父类方法,objc_super结构体中接收者为当前对象,并不是父类对象。所以打印接受者的类就是WYStudent。self是隐藏参数,表示当前对象,所以当前对象调用class方法,返回的对象的类WYStudent。

验证结果:

2021-10-15 20:29:42.676387+0800 消息发送[5275:104688] 父类:WYStudent---子类:WYStudent

2.3 小结

  • 方法的本质就是消息发送
  • 使用objc_msgSend发送当前类的消息
  • 使用objc_msgSendSuper发送父类的消息

3、快速查找

快速查找流程是从当前类的cache中查找imp。cache中存储有sel和imp,可以快速的通过sel查找到相应的imp。

3.1 查找源码

从上文我们知道方法调用其实底层是在执行objc_msgSend函数,所以从objc_msgSend函数开始入手查找。

  • 在objc源码中全局搜索objc_msgSend,找了一圈发现没有找到这个函数实现,这是因为cache的查找是通过汇编完成的
  • 所以需要全局搜索_objc_msgSend,找到了入口


    _objc_msgSend入口.png

3.2 汇编源码分析

3.2.1 整体概括

可能有人看汇编实现会犯怵,为了方便大家,我这里先对整体的过程进行说明,之后再看汇编如何实现

在这里涉及到cache的结构,务必先熟悉OC类的底层分析中的cache部分。

下面这幅图囊括了cache查询流程、重要的汇编实现、寄存器的变化过程,后面有看的不好理解的地方可以对照这幅图来看。


07-cache的方法查找流程.png
  1. 开始查询,得到Class
    1. 通过self的isa获取到该对象的Class
  2. 得到buckets
    1. 通过Class获取到类中的cache
    2. 得到cache中的buckets和mask
  3. 哈希算法查找
    1. 通过mask和方法选择_cmd进行哈希计算得到bucket所在的地址值
    2. 取出bucket中的imp
  4. 哈希冲突算法查找
    1. 如果发生冲突,开始哈希冲突算法
    2. 先以当前的bucket不断向前查找,判断是否是我们需要的bucket
    3. 如果一直找到第一个bucket,仍然没有找到,则跳转到最后一个bucket,继续向前查找
    4. 一直查找到第一个元素仍然没有找到,就开始慢速查找

3.2.2 开始查询,得到Class

源码:

//此处开始进入到msgSend流程
 ENTRY _objc_msgSend//以后看到ENTRY就说明了一个流程的开始
 UNWIND _objc_msgSend, NoFrame//没有视图

//检测是否为空和是否是小对象
//cmp表示比较。
//p0表示第1个寄存器的位置的相当于变量的东西,此处存储的是消息接受者。这还是因为当传入的时候,第一个参数就是消息接受者。
//第二个参数是方法选择器_cmd,所以p1肯定就是_cmd
//如果函数要返回值的时候,第一个寄存器存放的是返回值。
 cmp p0, #0   // nil check and tagged pointer check
//如果支持小对象类型。要么返回小对象或为空,
//b是进行跳转
//b.le是小于判断,也就是小于的时候LNilOrTagged
#if SUPPORT_TAGGED_POINTERS
 b.le LNilOrTagged  //  (MSB tagged pointer looks negative)
//b.eq是等于的是执行
#else
 b.eq LReturnZero //如果不支持小对象,则直接返回空
#endif
//这里是肯定存在的流程。
//ldr是存放一个值到一个变量中,
//刚才所看到的p1是消息接受者,x0就是这个p1寄存器所存储的内容,类的第一个属性就是isa,所以直接就是将isaf保存到了p13中
 ldr p13, [x0]     // p13 = isa
//此处是相当于方法调用,在汇编中是宏定义
//它的作用是取出isa中保存的Class 信息并保存到p16寄存器中
//也就是p16 = isa(p13) & ISA_MASK
 GetClassFromIsa_p16 p13  // p16 = class
//LGetIsaDone是一个入口
LGetIsaDone:
 // calls imp or objc_msgSend_uncached
    //接下来就是进入到缓存查找或者没有缓存查找方法的流程
    //这里传入的参数是NORMAL
 CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
 b.eq LReturnZero  // nil check判空处理,直接退出

 // tagged
 adrp x10, _objc_debug_taggedpointer_classes@PAGE
 add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
 ubfx x11, x0, #60, #4
 ldr x16, [x10, x11, LSL #3]
 adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
 add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
 cmp x10, x16
 b.ne LGetIsaDone

 // ext tagged
 adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
 add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
 ubfx x11, x0, #52, #8
 ldr x16, [x10, x11, LSL #3]
 b LGetIsaDone//前往z查找IMP
// SUPPORT_TAGGED_POINTERS
#endif

汇编语句解读:

  1. ENTRY _objc_msgSend
    • 此处开始进入到msgSend流程
    • 以后看到ENTRY就说明了一个流程的开始
  2. UNWIND _objc_msgSend, NoFrame
    • 表示没有视图
  3. cmp p0, #0
    • 判断p0的值是否为空
    • cmp表示比较
    • p0是self,此处存储的是消息接受者。这是因为当传入的时候,第一个参数就是消息接受者
    • 当函数返回时,p0是返回值
  4. if SUPPORT_TAGGED_POINTERS
  • 是否支持小对象类型
  1. b.le LNilOrTagged
    • b表示进行跳转
    • b.le是小于判断,也就是小于的时候开始执行LNilOrTagged
  2. b.eq LReturnZero
    • b.eq判断是等于的是执行
    • 如果为0,则直接退出
  3. ldr p13, [x0]
    • 将isa保存到了p13中
    • 刚才所看到的p0是消息接受者,x0就是这个p1寄存器所存储的内容,类的第一个属性就是isa
    • ldr表示将x0值传递到p13上
  4. GetClassFromIsa_p16 p13
    1. 将Class信息保存到p16
    2. 它的作用是取出isa中保存的Class 信息并保存到p16寄存器中
    3. 也就是p16 = isa(p13) & ISA_MASK
    4. 此处是相当于函数调用,在汇编中是一个宏定义
  5. CacheLookup NORMAL, _objc_msgSend
    1. 开始缓存查找流程

代码逻辑:

1、先拿到传入的消息接受者,判断是否为空
2、不为空则判断是否为小对象类型,如果是小对象类型则执行其他操作
3、获取到isa存储到p13,再获取到isa中的类信息存储到p16
4、开始进行缓存查找

3.2.3 缓存查找流程

源码:

/*
 此处就是在cache中通过sel查找imp的核心流程
*/
.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

/*
     ldr表示将一个值存入到p11寄存器中
     x16表示p16寄存器存储的值,当前是Class
     #数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
     #define CACHE (2 * __SIZEOF_POINTER__)
     经计算,p11就是cache
     */
    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]  //偏移16个字节,也就是取到cache_t          // p11 = mask|buckets

//真机64位看这个
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
     and表示与运算,将与后的值保存到p10寄存器
     p10为buckets
     */
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets,后48位为buckets
/*
     LSR表示逻辑向右偏移
     p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
     and p12,p1,p11,LSR #48表示_cmd &mask并保存到p12
     x12 = _cmd & mask
     这个是哈希算法,p12存储的就是搜索下标(哈希地址)
     */
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask,存储的是哈希地址
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

/*
 得到计算出的bucket并存放到p12中
 PTRSHIFT经全局搜索发现是3,
 LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
 通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p12中,
 
 哈希算法_cmd & mask
 这里是通过内存平移到达我们计算出的bucket
 想要获取buckets中某个下标的bucket,就需要进行内存平移
 而每个bucket结构体包含的是sel和imp,因此包含了16位,所以需要向左平移16位
 下标*16,就是buckets内存平移的大小得到查询的地址
 */
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

//分别将imp和sel存入到p17和p9
/*
     ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
     */
    ldp p17, p9, [x12]      // {imp, sel} = *bucket

//上面都是获取到sel和imp,下面进行比较
//哈希冲突算法
/*
 sel与传入的_cmd判断是否相同,相同就拿到这个sel对应的imp,不同则继续查找
 cmp表示比较
 b.ne表示如果不相同则跳转到2f
 如果相同则调用CacheHit,查找imp
 */
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
/*
 判断获取的是否是第一个元素,
 第一个元素跳转到最后一个元素,如果不是,则向前查找
 */
/*
 循环遍历查找sel
 通过p12和p10来判断是否是第一个bucket
 如果是第一个,则进入到3f
 如果不是,则获取到前一个bucket的sel继续执行第一个判断
 x12是寄存器p12的地址,减去一个bucket的大小就等于了前一个bucket的地址
 这里是通过反向查找,所以需要--
 通过ldp拿到这个地址的sel和imp
 */

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets 如果是第一个元素
    b.eq    3f          //拿到最后一个元素比较
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket ,否则向前查找
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
 直接拿到最后一个元素,因为Mask = capacity-1
 拿到最后一个元素后进行第二次递归查找
 */
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)  拿到最后一个元素赋给p12
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

//当跳转到最后一个元素时的第二轮遍历
    ldp p17, p9, [x12]      // {imp, sel} = *bucket  //再重新获取imp和sel
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0  //如果
    cmp p12, p10        // wrap if bucket == buckets//如果查找到第一个仍然没有查找到,就退出循环,走查询失败流程
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

汇编语句解读:

  1. .macro CacheLookup
    1. 此处表示CacheLookup的定义
    2. 以后方法的定义可以通过.macro来查找
  2. ldr p11, [x16, #CACHE]
    1. 将Class信息偏移16个字节,就获取到了cache
    2. 将缓存信息cache存入到p11寄存器中,p11 = mask|buckets,p11就是cache
    3. ldr表示将一个值存入到p11寄存器中
    4. x16表示p16寄存器存储的值,当前是Class
    5. “#数值”表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节。#define CACHE (2 * SIZEOF_POINTER )
  3. and p10, p11, #0x0000ffffffffffff
    1. 得到buckets,存储到p10
    2. p10 = mask|buckets & #0x0000ffffffffffff
    3. buckets占有后48位(真正数据是44位)
    4. and表示与运算,将与后的值保存到p10寄存器
  4. and p12, p1, p11, LSR #48
    1. p12是哈希地址,通过哈希算法得到,x12 = _cmd & mask
    2. LSR表示逻辑向右偏移,p11, LSR #48表示maskAndBuckets向左平移48位,也就是得到mask
    3. and p12,p1,p11,LSR #48表示_cmd &mask并保存到p12
  5. add p12, p10, p12, LSL #(1+PTRSHIFT)
    1. 得到计算出的bucket并存放到p12中
    2. PTRSHIFT经全局搜索发现是3,LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
    3. p10此时是第一个bucket的地址,需要通过地址平移来得到下标所在的bucket。
    4. 而平移的地址大小就是坐标*16(因为bucket包含sel和imp,所以是16个字节,所以需要乘以16)
    5. 所以此处是将首地址移动下标*16的位数就得到了计算出的bucket,并且放到p12中
  6. ldp p17, p9, [x12]
    1. 将imp和sel分别存储到p17和p9
  7. cmp p9, p1
    1. 比较bucket中的sel与传入的_cmd是否相等
  8. cmp p12, p10
    1. 比较bucket与buckets第一个bucket是否一致,也就是是否是第一个bucket
  9. ldp p17, p9, [x12, #-BUCKET_SIZE]!
    1. 将bucket的地址向前平移一个bucket的大小
    2. 也就是得到前一个bucket,并且继续将sel和imp存储到p17和p9中
    3. BUCKET_SIZE经全局搜索发现是一个bucket的大小
  10. add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
    1. 如果判断是第一个bucket就挪到最后一个位置
    2. 将cache向右平移44位,也就是mask向左平移4位,也就是mask*16
    3. 移动mask个bucket,所以是移动到了最后一位,这是因为mask=容量-1,所以mask就是最后一个位置,再乘上16,就是最后一个位置到第一个位置的地址大小了
    4. 经查询PTRSHIFT为4
    5. 这里也就是说明了为什么要把maskAndBuckets中间留下4位的0,就是为了在此处更好的计算

代码逻辑:

  1. 将Class从首地址内存平移16位得到cache,并存入到p11中
  2. 将cache进行掩码计算得到buckets,并存入到p10中
  3. 将maskAndBuckets右移48位得到mask与上_cmd得到哈希地址,并存入到p12中(哈希算法)
  4. 通过buckets平移到这个下标地址得到目标bucket,并存入到p12中
  5. 将目标bucket的imp和sel分别存入到p17和p9中
  6. 判断当前的bucket->sel != _cmd
    1. 如果查找到,则获取到imp返回
    2. 如果查找不到,就开始第一轮循环遍历(哈希冲突算法)
  7. 判断bucket是否是第一个元素,如果不是,则向前移动一位,再进行比较
  8. 如果是第一个元素,则跳转到最后一个元素,开启第二轮循环循环向前移动一位进行比较。
  9. 如果第二轮循环直到查找到第一个元素仍然没有找到,说明该方法确实不存在cache中,就到类的方法列表中查找。

3.2.4 开始获取imp

如果查找到了,就开始执行CacheHit。
源码:

.macro CacheHit
.if $0 == NORMAL
//验证并得到imp
 TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
 mov p0, p17
 cbz p0, 9f   // don't ptrauth a nil imp
 AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret    // return IMP
.elseif $0 == LOOKUP
 // No nil check for ptrauth: the caller would crash anyway when they
 // jump to a nil IMP. We don't care if that jump also fails ptrauth.
 AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP
 ret    // return imp via x17
.else
.abort oops
.endif
.endmacro

说明:
开始获取IMP,会执行TailCallCachedImp x17, x12, x1, x16。

3.2.5 开始去查找方法列表

3.2.5.1 CheckMiss

源码:

.macro CheckMiss
 // miss if bucket->sel == 0
.if $0 == GETIMP
 cbz p9, LGetImpMiss
.elseif $0 == NORMAL
 cbz p9, __objc_msgSend_uncached //进入到没有缓存的慢速查找
.elseif $0 == LOOKUP
 cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

说明: 如果没有找到,则开始进入慢速查找,进入到__objc_msgSend_uncached

3.2.5.2 __objc_msgSend_uncached

源码:

STATIC_ENTRY __objc_msgSend_uncached
 UNWIND __objc_msgSend_uncached, FrameWithNoSaves

 // THIS IS NOT A CALLABLE C FUNCTION
 // Out-of-band p16 is the class to search
 
 MethodTableLookup
 TailCallFunctionPointer x17

 END_ENTRY __objc_msgSend_uncached

说明: 这里可以看到跳转到了__objc_msgSend_uncached,在这里最终会执行MethodTableLookup来查询方法列表

3.2.5.3 MethodTableLookup

源码:

.macro MethodTableLookup
 
 // push frame
 SignLR
 stp fp, lr, [sp, #-16]!
 mov fp, sp

 // save parameter registers: x0..x8, q0..q7
 sub sp, sp, #(10*8 + 8*16)
 stp q0, q1, [sp, #(0*16)]
 stp q2, q3, [sp, #(2*16)]
 stp q4, q5, [sp, #(4*16)]
 stp q6, q7, [sp, #(6*16)]
 stp x0, x1, [sp, #(8*16+0*8)]
 stp x2, x3, [sp, #(8*16+2*8)]
 stp x4, x5, [sp, #(8*16+4*8)]
 stp x6, x7, [sp, #(8*16+6*8)]
 str x8,     [sp, #(8*16+8*8)]

 // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
 // receiver and selector already in x0 and x1
 mov x2, x16
 mov x3, #3
 bl _lookUpImpOrForward

 // IMP in x0
 mov x17, x0
 
 // restore registers and return
 ldp q0, q1, [sp, #(0*16)]
 ldp q2, q3, [sp, #(2*16)]
 ldp q4, q5, [sp, #(4*16)]
 ldp q6, q7, [sp, #(6*16)]
 ldp x0, x1, [sp, #(8*16+0*8)]
 ldp x2, x3, [sp, #(8*16+2*8)]
 ldp x4, x5, [sp, #(8*16+4*8)]
 ldp x6, x7, [sp, #(8*16+6*8)]
 ldr x8,     [sp, #(8*16+8*8)]

 mov sp, fp
 ldp fp, lr, [sp], #16
 AuthenticateLR

.endmacro

说明:

  • 其他的不用看,可以看到最终会跳转到_lookUpImpOrForward,
  • 而通过注释可以看到传入的参数为(obj,sel,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER),也就是说behavior为0011
  • \color{red}{_lookUpImpOrForward这个方法很重要,接下来慢速查找就从它开始。}

3.3 总结分析

代码执行流程:


07-cache的方法查找流程.png

寄存器的存储示意图:


寄存器存储流程.png

3.4 代码逻辑难点解析

3.4.1 为什么有两次循环遍历?

  • 第一次循环是从当前bucket向前查找循环,第二种是从最后一个bucket向前循环。

  • 这是cache存储的哈希冲突算法决定的,哈希冲突算法就是先向前查询,如果查到了第一个就从最后一个再继续查询,一直查到刚才的位置

  • 哈希冲突算法:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;//判断如果i存在,将下标-1,也就是向前一位存储,如果为0,也就是计算到第一个位置,就直接放到mask,也就是最后一个位置
}

3.4.2 如何人为的将当前bucket设置为buckets的最后一个元素?

  • 通过buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker的最后一个元素

  • mask = bucket总数-1,因此偏移mask的16字节,就跳转到最后一个bucket


    maskAndBuckets结构图.png
  • 这里的后4位就是0,因为其实buckets存储的是后44位,中间这4位就是为现在使用

  • mask加上后四位,就是平移到最后一个bucket的地址大小

3.4.3 如何查找相应的bucket

通过哈希算法,_cmd与mask

哈希算法

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

3.4.4 为什么在通过哈希算法计算了哈希地址后,还要循环遍历,而不是直接判断就结束了

因为插入的时候遇到哈希冲突会进行哈希冲突算法,所以在查询的时候为了预防哈希冲突而存入其他的位置,所以就再需要循环向前查找。

3.5 关于汇编的几个小问题

1、为什么objc_msgSend底层使用汇编

  • 汇编实现特别快
  • 具备参数的不确定性,而C或C++更具有确定性,去实现动态参数更麻烦。

2、怎么查找汇编文件

  • 汇编的后缀名是.s
  • 由于我们日常开发的都是架构是arm64,所以需要在arm64.s后缀的文件中查找

3、汇编的函数前面都带有,如果要在C文件中查找要把这个删掉,比如_objc_msgSend

3.6 总结

  1. 在消息发送时,先在本类的cache中查找是否存在,谓之快速查找
  2. 先通过接受者对象得到Class,再查看Class中的cache,之后拿到cache中的bucket的sel与传入的_cmd进行比较,如果存在则返回imp。
  3. 这里查找bucket的过程涉及到哈希算法,先通过哈希算法_cmd &mask得到哈希地址的bucket
  4. 如果不存在,可能会是哈希冲突之后的地址,就先向前查找,如果找到第一个位置还没找到,就从最后一个向前查找,一直找到第一个元素,最终没找到就开始慢速查找了
  5. 简单的逻辑就是sel->isa->Class->cache->buckets(_cmd &mask)->通过下标获取到每个buckets->获取到sel和imp->进行比较

4、慢速查找

慢速查找流程:在快速查找流程中未查找到IMP,就会进入到类的方法列表以及父类的cache和方法列表中继续查询,这个过程就是慢速查找流程。

底层是通过C语言实现。

在进行快速查找的过程中我们已经发现了最终会进入到lookUpImpOrForward去执行,因此我们就从这个函数开始分析。

主要内容包括整体流程、二分查找法、父类查找流程、动态方法解析。这里不对动态方法解析进行分析,会在下一章进行详细说明。

慢速查找流程在对方法列表的查询过程涉及到了类、分类的方法列表的构造方式,后续会详细分析类和分类的加载过程,会解析方法列表是如何构造的,在这里遇到关于方法列表的构造内容可以先记下不用深究。

4.1 整体流程

4.1.1 源码分析

源码:

/*
 1、如果是从汇编中进来,也就是cache中没有找到imp,则behavior为0011,LOOKUP_INITIALIZE | LOOKUP_RESOLVER
 2、如果是通过lookUpIMpOrNil进来的,behavior为1100,behavior | LOOKUP_CACHE | LOOKUP_NIL
 3、如果是class_getInstanceMethod进来的,也就是仅仅在查询方法列表时,behavior为0010,LOOKUP_RESOLVER
 4、在动态解析过程中会通过resolveMethod_locked调用:behavior为0100,behavior | LOOKUP_CACHE
 */

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    /*
      method lookup
     enum {
         LOOKUP_INITIALIZE = 1, 0001
         LOOKUP_RESOLVER = 2,   0010
         LOOKUP_CACHE = 4,      0100
         LOOKUP_NIL = 8,        1000
     };
     behavior是其中的某个值
     因此behavior与这几个数相与,只有相等,才会不为0,如果不相等肯定会为0,以此来判断是否是这几个枚举值
     */
    
    
    //消息转发(报错方法)
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    //多线程
    /*
     这里是从动态方法解析的过程中来的
     也就是说明此处的方法调用是查找缓存
     */
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    //是否为已知类,也就是是否已经被加载到内存中
    checkIsKnownClass(cls);

    //类的实现,(也就是是否将类的数据按照类的结构构造完成),需要将类和元类的继承链都要实现一下
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    /*
        当从cache中没有查找到进入该方法时,behavior为0011,
        behavior & LOOKUP_INITIALIZE说明此处是进行查找初始化方法
        初始化,执行initialize函数
        这里可以看出只有在查找方法列表时才会调用initialize函数
     
        所以条件为:1)cache中没找到进入到方法列表中查找方法;2)且该类还没有被初始化
     */
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    // unreasonableClassCount -- 表示类的迭代的上限
    /*
     1、类的方法查找
     2、查找父类为nil
     3、for循环用来查询父类的方法列表
     */
    //这个for循环用来循环查询父类
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            //查找到,就返回imp,并存放到cache中
            imp = meth->imp;
            goto done;
        }

        /*
         1、给cureClass赋值superclass
         2、判断父类如果为nil,也就是NSObject的父类为nil,就开始默认转发
         */
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        //循环如果达到上限了,就提示内存损坏,不再执行
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        //得到父类的imp(从缓存中查找),最终返回只能是cache中存储的imp
        imp = cache_getImp(curClass, sel);
        
        //如果父类缓存中得到报错函数,就直接返回,找初始类的动态方法解析和消息转发
        //这里如果是报错函数,直接跳出开始默认转发
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            /*
             如果在父类中查找到了报错函数,就停止搜索,并且不进行缓存,开始对当前类进行动态方法解析
             */
            break;
        }
        //如果父类存在该方法,则存入到初始类缓存中
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    //当上边的循环中遇到break退出循环时进入到这里
    // No implementation found. Try method resolver once.当没有查找到Imp时,尝试一次动态方法解析
    /*
     
     当从动态方法解析后再次进入该方法时,behavior为1100
     而LOOKUP_RESOLVER为0010,所以就不会进入。
     */
    //behavior这个作为标识,只能进一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //这里是异或操作,不相等为1,相等为0,
        //如果如果可以进入这里,说明是xx1x,异或一下之后就变成xx0x
        behavior ^= LOOKUP_RESOLVER;
        //动态方法解析
        //这里的返回值不会是nil,如果查询不到返回的是forward_imp
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

    //通过查看done发现如果没有查找到,不会存储进cache中,也就是说这里是不会存入forward_imp
    //只有查找到imp才会进入到done
 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    //如果behavior为1xxx,与1000相与,就为YES,此时再加上查不到imp,就会返回nil
    //只有一种情况,那就是动态方法解析之后再次执行该函数,此时在cache中查询得到的是forward_imp,就会返回nil
    //这里很疑惑的一点,什么情况下会把forward_imp存入到缓存中
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    //如果是动态方法解析完成后再进入该方法一定会执行done_nolock,因为return是在done_nolock下面的
    return imp;
}

代码分析:

  1. 前期准备
    1. 判断cache_t中是否已经有该sel和imp
    2. 判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类(类加载时会分析)
    3. 将该类的继承链和该类的元类继承链的相关类都实现一遍,方便后续进行父类的查找、类方法的查找(类加载时会分析)
    4. 判断该类是否初始化,如果没有则进行初始化, 包括该类的继承链和该类的元类的继承链都进行初始化
  2. 方法查询
    1. 得到sel对应的Method
    2. 如果存在,直接拿到IMP
    3. 判断父类如果为空,则开始报错,如果父类存在,则查找父类的方法
  3. 父类方法查询
    1. 此处又开始在汇编中查找父类的cache,需要在汇编中查找
    2. 父类的报错方法
    3. 查找到IMP存在,则继续执行,存储到当前类的cache中
  4. 方法动态解析(先不处理)

4.1.2 整体流程

整体流程:
1、初始化类->方法列表查询->循环父类查询->动态方法解析
2、查询到方法后需要保存到当前类的cache中
3、如果父类返回报错函数或者父类为nil时返回报错函数,此时就赋值为报错函数,并开始动态方法解析

快速查找:
当动态解析之后再次进来时会先进行快速查找,避免经过动态方法解析后已经有了方法,在其他线程已经将方法插入缓存中

初始化:
1、包括类的加载、类的实现、类的初始化
2、当在方法列表中调用方法时,如果这个类从来没有调用过initialize函数,此时就会调用initialize函数。
3、为什么整个的继承链、元类的继承链都要实现一下,因为还要找类方法、父类方法
4、Class是双向链表结构,父类保存有自己的子类,子类保存有自己的父类

方法列表查询:
1、方法列表查询采用二分查找算法实现的
2、方法列表在加载的时候就已经排好序了(通过方法的sel的地址进行排序),因此可以使用二分查找法来快速查找
3、这个二分查找比较好,因为它的中间位置都是通过起始位置计算的。而后面更改只需要更改起始位置就可以了

父类查询:
1、当前类的cache和methodList都没有查询到,就开始循环遍历父类的cache和methodList
2、如果是父类查询到该方法,需要保存到本类的cache
3、父类的cache也会进入到汇编中进行
4、父类的methodList的循环是通过for循环实现的

示意图:

08-慢速查找流程.png

小结:
1、先判断类的加载、类的实现、类的初始化,如果都完成,就可以开始查询methodList了
2、methodList的查询是通过二分查找法实现的,二分查找法的前提条件是进行排序,在类的实现过程中就已经进行了排序了。(排序是通过sel的地址来排的)
3、通过for循环来进行父类的查找,先查找父类的cache,再查找父类的methodList,cache也是通过汇编拉查找的
4、一直查询到父类为nil时,或者返回的是一个报错方法,就拿到报错方法,并进行方法的动态解析和消息转发

4.2 二分查找法

4.2.1 getMethodNoSuper_nolock

源码:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    //得到当前类的方法列表
    auto const methods = cls->data()->methods();
    //循环遍历得到对应的Method
    //一个方法列表数组里有多个方法列表,详情可以看后续类的加载过程,方法列表是如何加载的
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        //进行二分查找法
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

说明:
1、我们知道方法列表存储在类的rw中,所以可以通过cls->data()->methods来获取,如果了解类的底层结构,这一点很简单
2、至于为什么这里有很多方法列表呢?

  • 首先在类的底层结构中我们可以看到通过methods函数获取的是方法列表数组,方法列表数组中存放的所有的方法列表。方法列表中存储的是每个方法。
  • 所以需要先对方法列表数组进行循环遍历得到每个方法列表,之后再对方法列表进行二分查找

4.2.1 findMethodInSortedMethodList

通过search_method_list_inline调用到findMethodInSortedMethodList,具体的二分查找算法就在findMethodInSortedMethodList函数中

源码:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;//查找指针
    uintptr_t keyValue = (uintptr_t)key;//需要查找的方法选择器
    uint32_t count;
    
    /*
     每次循环,查找的数量是之前的一半(如果除不整,就向下取整)
     base是起始位置,probe是中间位置,count是本轮最大数量
     每次循环都要将count/2
     */
    for (count = list->count; count != 0; count >>= 1) {
        /*
         起始位置偏移整体数量的一半,就移动到了中间位置
         每一次循环需要让起始位置偏移count/2的位置得到中间位置
         */
        probe = base + (count >> 1);//右移1位,就是count/2
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //这里是为了获取后插入的类别的同名方法,
        //后插入的在前面
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //如果不是第一个,而且probe的前一个也有这个name,向前查找
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        //如果在右侧,则将起始位置放置到probe+1,(count-1)/2,这里减一是因为把之前的probe也减去了
        /*
         如果在中间位置的右侧,需哟啊改变起始位置
         改变后需要将数量-1,因为起始位置偏移后,占据了一个位置,所以总数要减少1
         */
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

说明:
只是一个简单的算法没有难度,而且代码层面注释已经写得足够详细了,也就不展开多说了。这里说下算法注意点

注意点

  1. 比较的只是中间位置,前后位置不需要比较,而中间位置是通过起始位置和总数来进行计算的
  2. 总数的计算,总数每次都要变,也就是除以2,向右位移1位
  3. 起始位置的计算
    1. 如果实际位置在中间位置右侧,则需要改变起始位置,也就是中间位置probe+1,因为把probe也减去了,所以总数还要先减一,后面再去除以2.
    2. 如果实际位置在中间位置左侧,就不需要改变
  4. 这里还有一个比较不好想象的小细节,就是如果没有中间位置,比如中间只有偶位数,中间位置何去何从呢
    1. count>>1,这个是简单计算一下就会发现最后一位是会抹去的,也就是说会向下取整
    2. 也就是如果是偶位数,那么中间位置会是中间偏后的那个。
      5、多个分类的加载,越迟加载的越在前边,所以需要向前查找,已得到分类的同名方法。

小结:
1、方法列表中查找采用二分查找法查找
2、分类比类加载要晚,所以分类的方法会在类的方法前面,而且越迟加载的分类越在前面
3、因此我们在查找方法列表时当查找到方法时,会继续向前查找分类的同名方法。
4、排序是通过方法选择器地址来排的,判断方法是通过方法名来判断的。这个要记住,很多人会认为排序是通过方法名来排的,其实并不是。

4.3 父类查找流程

父类的流程重点在于先通过汇编查找cache,之后再回来C语言中查找方法列表。

cache缓存查找太简单,通过cache_getImp在汇编中一搜,之后一步一步往下走即可。这里就不再赘言了。

源码:

//这个for循环用来循环查询父类
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            //查找到,就返回imp,并存放到cache中
            imp = meth->imp;
            goto done;
        }

        /*
         1、给cureClass赋值superclass
         2、判断父类如果为nil,也就是NSObject的父类为nil,就开始默认转发
         */
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        //循环如果达到上限了,就提示内存损坏,不再执行
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        //得到父类的imp(从缓存中查找),最终返回只能是cache中存储的imp
        imp = cache_getImp(curClass, sel);
        
        //如果父类缓存中得到报错函数,就直接返回,找初始类的动态方法解析和消息转发
        //这里如果是报错函数,直接跳出开始默认转发
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            /*
             如果在父类中查找到了报错函数,就停止搜索,并且不进行缓存,开始对当前类进行动态方法解析
             */
            break;
        }
        //如果父类存在该方法,则存入到初始类缓存中
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

说明:

  1. 简单的循环流程
    1. 先查找当前类的methodList
    2. 再查找父类的cache
    3. 再查找父类的methodList
  2. cache_getImp方法是通过汇编_cache_getImp实现,传入的$0 是 GETIMP
  3. 如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,则直接返回imp
  4. 如果在父类缓存中,没有找到方法实现,则跳转至CheckMiss 或者 JumpMiss,通过判断$0 跳转至LGetImpMiss,直接返回nil

流程示意图:

08-父类查找流程.png

小结:

  1. 当前类查询methodList没有找到后,会先查找父类的cache
  2. 如果父类的cache没有找到,就查找父类的方法列表
  3. 如果父类仍然没有找到,继续找父类,一直到父类为nil,当父类为nil时,说明当前类是NSObject类。也就是找到头了,此时会退出开始开始进行动态方法解析。
  4. 父类查找到方法后会存储在当前类的cache中。

4.4 总结

  1. 慢速查找流程先查找类的方法列表,再查找父类的cache和方法列表,一直找到NSObject类仍然没有找到就开始动态方法解析,解析完成后会再次查找方法列表。如果仍然没有找到,就返回forward_imp,也就是报错函数。如果找到就存储到当前类的cache中。
  2. 方法列表的查找是通过二分查找法来实现的,当查找到时会继续在方法列表中向前查找,寻找分类中的同名方法。

5、动态方法解析

在上文可知,lookUpImpOrForward函数中,当前类以及父类都没有查找到方法时,会break跳出循环,开始执行动态方法解析,下面就开始分析动态方法解析的过程。

5.1 整体说明

以前我们都知道动态方法解析的做法就是通过resovleInstanceMethod方法或resolveClassMethod方法来给原来的sel动态的增加一个imp。增加后再去cache中或方法列表中查询就可以正确的给sel发送消息了。

所以我们接下来的任务就是查看在底层是如何实现这一过程的。

5.2 behavior的认识

我们看到在进入lookUpImpOrForward函数时,会传入一个参数behavior,在代码中会通过这个参数来判断哪些代码需要执行,哪些不需要执行,所以为了看懂代码流程,一定要理解behavior是怎么使用的

定义:

method lookup
     enum {
         LOOKUP_INITIALIZE = 1, 0001
         LOOKUP_RESOLVER = 2,   0010
         LOOKUP_CACHE = 4,      0100
         LOOKUP_NIL = 8,        1000
     };

可以看到有四种类型,behavior与这几个数相与,只有相等,才会不为0,如果不相等肯定会为0,以此来判断是否是这几个枚举值。

共有初始化、动态方法解析、缓存查找、返回nil四种执行判断

通过全局搜索查找可以看到不同的地方调用该函数,这四种执行分别是否会执行。

1、如果是从汇编中进来,也就是cache中没有找到imp,则behavior为0011,LOOKUP_INITIALIZE | LOOKUP_RESOLVER,可以进行初始化和动态方法解析。
2、如果是通过lookUpIMpOrNil进来的,behavior为1100,behavior | LOOKUP_CACHE | LOOKUP_NIL,可以进行cache查找和返回nil.
3、如果是class_getInstanceMethod进来的,也就是仅仅在查询方法列表时,behavior为0010,LOOKUP_RESOLVER,可以进行动态方法解析。(\color{red}{ 这个方法在消息转发的消息重定向中会调用) }
4、在动态解析过程中会通过resolveMethod_locked调用:behavior为0100,behavior | LOOKUP_CACHE,可以进行动态方法解析。

具体使用:

LOOKUP_CACHE

// Optimistic cache lookup
   //多线程
   /*
    这里是从动态方法解析的过程中来的
    也就是说明此处的方法调用是查找缓存
    */
   if (fastpath(behavior & LOOKUP_CACHE)) {
       imp = cache_getImp(cls, sel);
       if (imp) goto done_nolock;
   }

LOOKUP_INITIALIZE

/*
       当从cache中没有查找到进入该方法时,behavior为0011,
       behavior & LOOKUP_INITIALIZE说明此处是进行查找初始化方法
       初始化,执行initialize函数
       这里可以看出只有在查找方法列表时才会调用initialize函数
    
       所以条件为:1)cache中没找到进入到方法列表中查找方法;2)且该类还没有被初始化
    */
   if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
       cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
       // runtimeLock may have been dropped but is now locked again

       // If sel == initialize, class_initialize will send +initialize and 
       // then the messenger will send +initialize again after this 
       // procedure finishes. Of course, if this is not being called 
       // from the messenger then it won't happen. 2778172
   }

LOOKUP_RESOLVER

 当从动态方法解析后再次进入该方法时,behavior为1100
    而LOOKUP_RESOLVER为0010,所以就不会进入。
    */
   //behavior这个作为标识,只能进一次
   if (slowpath(behavior & LOOKUP_RESOLVER)) {
       //这里是异或操作,不相等为1,相等为0,
       //如果如果可以进入这里,说明是xx1x,异或一下之后就变成xx0x
       behavior ^= LOOKUP_RESOLVER;
       //动态方法解析
       //这里的返回值不会是nil,如果查询不到返回的是forward_imp
       return resolveMethod_locked(inst, sel, cls, behavior);
   }

LOOKUP_NIL

//如果behavior为1xxx,与1000相与,就为YES,此时再加上查不到imp,就会返回nil
   //只有一种情况,那就是动态方法解析之后再次执行该函数,此时在cache中查询得到的是forward_imp,就会返回nil
   //这里很疑惑的一点,什么情况下会把forward_imp存入到缓存中
   if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
       return nil;
   }

5.3 lookUpImpOrForward中开始进入动态方法解析

源码:

 //当上边的循环中遇到break退出循环时进入到这里
    // No implementation found. Try method resolver once.当没有查找到Imp时,尝试一次动态方法解析
    /*
     
     当从动态方法解析后再次进入该方法时,behavior为1100
     而LOOKUP_RESOLVER为0010,所以就不会进入。
     */
    //behavior这个作为标识,只能进一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //这里是异或操作,不相等为1,相等为0,
        //如果如果可以进入这里,说明是xx1x,异或一下之后就变成xx0x
        behavior ^= LOOKUP_RESOLVER;
        //动态方法解析
        //这里的返回值不会是nil,如果查询不到返回的是forward_imp
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

说明:

  1. 上边代码中如果通过break退出循环时,就会开始进入到动态方法解析。上文已经分析过,如果是没有查找到方法就会break。所以是当没有找到方法后开始进入动态方法解析
  2. 一个类每次进行慢速查找后只会执行一次动态方法解析
  3. 通过behavior来判断,避免动态方法解析之后去查询imp时再次进入,造成死循环
  4. 这里的behavior异或完之后变成了xx0x,之后作为参数传入,而在resolveMethod_locked中会再次调用lookUpImpOrForward中将behavior作为参数传入,所以再进入到该方法中不会再进入到动态解析了。
  5. 最终通过resolveMethod_locked函数来执行动态方法解析。

5.4 resolveMethod_locked中开始执行动态方法解析

源码:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    //不是元类,直接使用实例方法的解析
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    }
    //如果是元类,需要调用类方法
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //这里是因为isa继承关系中最终会走到NSObject,所以还需要一个实例方法
        //这个是resolveClassMethod里将类方法解析成类方法
        resolveClassMethod(inst, sel, cls);
        //这里还会再查询一遍,此时behavior为1100,因此不会再动态方法解析了
        /*
         这个sel在cache中查询到的imp是forward_imp,那么就返回nil
         */
        if (!lookUpImpOrNil(inst, sel, cls)) {
            //这个是resolveInstanceMethod里将实例方法解析成类方法
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //在调用方法解析的时机已经添加到了cache,所以需要去查询一下缓存的方法(chances这里应该表示为时机)
    //多线程,此时可能已经增加了该方法
    //可以看到这里的behavior为xx0x,所以本次的查询是不会进入到动态方法解析中了
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

说明:

  1. 如果是传入的类不是元类,则调用resolveInstanceMethod()来对实例方法进行解析
  2. 如果是传入的类是元类,则调用resolveClassMethod()来对类方法进行解析
  3. 解析完成后,会再次调用lookUpImpOrForward再次进行查询方法。可以看到这里的behavior为xx0x,所以本次的查询是不会进入到动态方法解析中了。
  4. 使用LOOKUP_CACHE,是考虑到多线程此时可能已经有其他线程执行了该方法

5.5 resolveInstanceMethod对实例方法进行动态方法解析

源码:

/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.可能是一个元类,也可能是一个非元类
* Does not check if the method already exists.
**********************************************************************/

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    //得到这个方法,系统提供的resolveInstanceMethod,需要自己实现
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //再去查询,发现没有实现resolveInstanceMethod就直接退出
    /*
     如果cls是元类,则根元类也是有resolveInstanceMethod的,所以也可以判断
     */
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //调用一个objc_msgSend函数执行resolveInstanceMethod:
    //如果返回一个YES
    /*
     如果cls是元类,也会执行resolveInstanceMethod函数
     也就是说如果查找的是类方法,也会进入到resolveInstanceMethod
     */
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //这里可以看到不管返回的resolved是不是YES,都会进行一次查询
    //再查询一次得到imp
    //此时虽然会查询一遍,但是因为lookUpImpOrNil函数中的behavior固定为1100,所以不会再次进行动态方法解析了
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    //只有yes才会进入
    if (resolved  &&  PrintResolving) {
        //进行打印
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

执行过程:
1、先判断元类或根元类中是否实现了resolveInstanceMethod
2、执行resolveInstanceMethod
3、再次对sel进行查询,通过lookUpImpOrNil

说明:

  1. 根据源码注释,也通过resolveMethod_locked函数中均可以看到resolveInstanceMethod传入的可能是类,也可能是元类,也就是它也可以进行类方法的动态解析。
  2. 会先执行一下resolveInstanceMethod方法。
  3. 执行后就再通过lookUpImpOrNil查询一遍imp,注意此时直接就查询,而不会看resolveInstanceMethod执行返回的是否是YES。
  4. resolveInstanceMethod方法的返回值只是为了打印,并没有其他作用,真正起作用的就是看到底有没有给这个sel增加imp
  5. lookUpImpOrNil函数中调用lookUpImpOrForward函数传入的behavior是0011,所以并不会再次进行动态方法解析了。
  6. 因此我们只需要在这个resolveInstanceMethod方法中给这个sel加上imp,就可以保证查询成功

lookUpImpOrNil

lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    //behavior | LOOKUP_CACHE | LOOKUP_NIL为1100
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

5.6 resolveClassMethod对类方法进行动态方法解析

源码:

/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.这里的cls是元类
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    //判断resolveClassMethod有没有实现,如果没有实现直接退出
    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);//返回元类的原始类(如果不是元类,就返回它自己)
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //消息发送,执行resolveClassMethod函数
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    //再一次进行消息发送
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            //返回为空,说明没有动态方法解析没有给这个sel添加imp
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

执行过程:
1、判断当前的元类中是否有resolveClassMethod方法
2、获取到该元类的原始类
3、执行resolveClassMethod方法
4、再一次进行消息发送,通过lookUpImpOrNil

说明:

  1. 这里传入的cls只是元类,不会是类
  2. 针对类方法进行动态方法解析
  3. 内部会执行resolveClassMethod方法
  4. 通过nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);获取到元类的初始类。
  5. 可以看到调用类方法,objc_msgSend函数中传递的接受者参数也必须是类,而不能是元类,虽然方法存储在元类中,但是消息接受者仍然是类。
  6. 执行完resolveClassMethod,再执行一次消息发送,查询imp

5.7 一些疑问的解答

1、为什么当cls是元类时,除了执行一次resolveClassMethod,当判断lookUpImpOrNil没有成功后,需要再执行一次resolveInstanceMethod。这是为啥呢?

首先我看到有人说这是因为类方法在元类中是以实例方法的姿态存在,如果是这样的话,那么用一种方式去做就行了,为什么要用两种呢,没必要,而且在resolveClassMethod也是通过获取到元类的原始类才去调用的方法呀,并不是直接用的元类去调用的,按照这种说法的话,通过resolveClassMethod没有获取到,就会再次通过resolveInstanceMethod来获取,然而实际调试发现并不会进入,说明此处是另有深意,并不是简单的第一种方式不行,再用第二种方式查询。

这个也不是因为元类的继承链链中有NSObject导致的,因为在resolveClassMethod也是拿到原始类直接调用的,而如果传入的不是元类,就直接用自己。

接下来需要看看调用lookUpImpOrNil返回nil条件。

查看lookUpImpOrNil函数

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    //behavior | LOOKUP_CACHE | LOOKUP_NIL为1100
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

说明:
这里可以看到,传入的behavior包含有LOOKUP_NIL。上文我们在分析behavior知道只有这里调用时才会判断LOOKUP_NIL。

再查看lookUpImpOrForward函数

cache查找

// Optimistic cache lookup
    //多线程
    /*
     这里是从动态方法解析的过程中来的
     也就是说明此处的方法调用是查找缓存
     */
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

说明: 这里也是唯一的可以进入到done_nolock的地方。并且当查询到imp后执行done_nolock

done_nolock

done_nolock:
    //如果behavior为1xxx,与1000相与,就为YES,此时再加上查不到imp,就会返回nil
    //只有一种情况,那就是动态方法解析之后再次执行该函数,此时在cache中查询得到的是forward_imp,就会返回nil
    //这里很疑惑的一点,什么情况下会把forward_imp存入到缓存中
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }

说明:

  • 在nonlock中如果imp为forward_imp,就直接返回nil
  • 此时的behavior为1100,所以(behavior & LOOKUP_NIL)肯定是1

总结:所以当动态解析之后cache中查询到的imp为forward_imp时会返回nil,此时会执行resolveInstanceMethod

5.8 几个没有理解的问题,有知道的可以评论告诉我

1、为什么传入的cls可以是元类呢,我们在调用方法时,传入的不都是类吗?谁会直接传元类呢,而且查看调用lookUpImpOrForward的地方也没找到哪里传的是元类,很奇怪。

2、在resolveInstanceMethod或resolveClassMethod中已经执行了一次lookUpImpOrNil了,为什么再执行一次lookUpImpOrForward呢?

猜测:

  • 虽然进行了方法动态解析,并且也通过lookUpImpOrNil再一次进行查询了
  • 但是上一次执行的lookUpImpOrForward方法的执行还没有返回值呢,所以需要在这里执行一下获取到返回值
  • 而在动态方法解析过程中的再一次查询的过程中可能已经缓存到cache中了,所以此时传入的behavior要包含LOOKUP_CACHE用来查询cache

5.9 简单验证

在WYStudent类中写有mehtodDynamically方法的声明,但是没有实现,在resolveInstanceMethod方法中判断mehtodDynamically的SEL,就将resolveInstanceMethodTest函数作为mehtodDynamically的函数实现。

代码:

//实例方法的动态方法解析
/*
 如果给该sel添加了imp,则直接执行
 如果没有添加成功,不管返回的YES还是NO,都会执行消息转发
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //mehtodDynamically方法在.h中声明了,但是没有在.m文件中定义
    if (sel == @selector(mehtodDynamically)) {
        //这里的参数很重要,具体情况具体看
        //给一个类添加方法,参数分别是类名、方法选择器、IMP,参数类型(涉及到类型编码)
        class_addMethod([self class],sel,(IMP)resolveInstanceMethodTest,"v@:");
        return YES;
    }
    if (sel == @selector(eat)) {
        return NO;
    }
    return [super resolveInstanceMethod:sel];
}

//这里的参数必须这样写,因为底层本来就是这样写的。保持一致。
void resolveInstanceMethodTest(id self,SEL _cmd){
    NSLog(@"大家好,我是一个被动态解析的作为mehtodDynamically实现函数");
}


//调用
//动态方法解析
WYStudent *student = [WYStudent alloc];
[student mehtodDynamically];

结果:

2021-10-17 14:58:39.232592+0800 消息发送[84736:1535620] 大家好,我是一个被动态解析的作为mehtodDynamically实现函数

5.10 总结

  1. 慢速查找会查找当前类的方法列表,如果方法列表也不存在,则开始查询父类的cache和方法列表
  2. 慢速查找过程:先查找当前类的方法列表,之后再查找父类的cache和父类的方法列表,一直到NSObject还没有查找到就开始进行动态方法解析,解析完成后会再次查找方法列表。如果仍然没有找到,就返回forward_imp,也就是报错函数
  3. 在方法列表中通过sel查找imp是通过二分查找来获取的
  4. 动态方法解析是通过resolveInstanceMethod和resolveClassMethod实现的。

6、消息转发

在上文我们从objc_msgSend开始查询,查到了cache流程、方法列表查找流程、动态方法解析流程,可是动态方法解析之后再次执行了lookUpImpOrForward,如果没有找到方法实现,会将报错函数的赋给imp,再继续找源码并没有发现消息转发相关的代码。所以这个方法调用的流程已经结束了,也就是objc_msgSend的流程结束了。

人们都说在动态方法解析之后会进行消息转发,那么是怎么来的呢?

查看官方文档时知道当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation:消息通知该对象。

因此我们可以在对这个对象进行通知时将消息转发给其它对象来实现。

注:

  • 因此在这个意义上说消息转发并不是消息发送的流程,真正消息发送的流程是objc_msgSend的过程,上文我们分析过了,最后走到了动态方法解析即结束了。
  • 而我们可以通过消息转发实现,是因为当消息发送失败后,系统会给这个对象发送一个通知。所以我们在这个通知中进行消息转发。
  • 消息发送是给这个对象发送一个消息,消息转发其实已经脱离了这个范畴。

6.1 消息转发的分析思路

虽然通过官方文档知道了在动态方法解析后如果仍然没有找到imp会进行消息转发来通知该方法,可是在源码中并没有找到该部分代码,那么应该怎么分析它具体的执行过程呢?

第一个比较容易想到的是通过反编译查看底层实现。因为上层苹果没有给我们提供源码实现,可是在编译时肯定是有的,我们通过反编译就可以看到这个过程。

还有一种方式就是查看在崩溃之前都执行了哪些方法,处在动态方法解析之后、报错方法之前的方法就是消息转发的方法。

6.1 hopper反编译的分析

反编译的使用,比较难看懂,如果再进行详细的介绍,博客就太大了...,因此我这里只进行简单的介绍,更详细的反编译的使用后面会写博客分析。

一般查看源码是查看如何将上层代码编译为底层代码的,也就是进行汇编后的效果,如果底层代码无法查看,就只能反汇编,从底层代码编成上层代码。

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

【第一步】:先拿到镜像文件

  • image list指令可以获取到所有的镜像文件路径


    镜像文件路径.png
  • 在路径中获取到镜像文件


    镜像文件.png

【第二步】:使用Hopper Disassembler打开镜像文件

反汇编界面.png

  • 全局搜索一下自己想要查找的方法,会进入这个界面
  • 接下来就可以通过方法来查找。

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

instrumentObjcMessageSends可以用来打印方法调用的信息,所以我们可以使用它来查看方法的调用过程,是否包含消息转发。

6.2.1 打开objcMsgLogEnabled开关

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

代码:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 需要将外部引用的导入进来,否则会报错,这样就告诉编译器到其他文件中加载这个文件
  • 打开开关也就是设置为YES
  • 打开后还需要设置为NO,避免影响其他地方

6.2.1 运行代码,并前往/tmp/msgSends 目录

消息发送日志路径.png
  • 通过logMessageSend源码,了解到消息发送打印信息存储在/tmp/msgSends 目录
  • 一次在运行后,就可以前往这个目录查找日志文件

6.2.1 查看日志文件

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

6.3 消息接受者重定向

在WYCat中创建有eat方法,并带有实现。在WYPerson中并没有这个方法,我们通过WYPerson来调用,看看是否可以将消息接收者重定向到cat。

WYCat源码:

@interface WYCat : NSObject

@property (nonatomic ,assign ,readonly) int age;
@property (nonatomic, copy,readwrite) NSString *name;

- (void)eat;

@end

@implementation WYCat

- (void)eat{
    NSLog(@"大家好,虽然我是cat,但我是被Person调用的");
}

@end

WYPerson源码:


@interface WYPerson : NSObject

- (void) runtimeTest;
- (void)mehtodDynamically;//没有方法实现
- (void)eat;
- (void)getCatProperty;
- (void) msgSendSuperTest;
@end

@implementation WYPerson
//返回接受者对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(eat)) {
        return [[WYCat alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
//    return nil;
}

@end

main调用

//消息接收者重定向
WYPerson *person = [WYPerson alloc];
[person eat];

运行结果:

2021-10-17 15:12:28.041183+0800 消息发送[85460:1551709] 大家好,虽然我是cat,但我是被Person调用的

总结:

可以看到当我们调用WYPerson的eat方法时,发现它并没有eat的函数实现。但是我们可以通过消息接收者重定向,判断当前方法是eat时,改变消息接受者为WYCat的对象,这样就会让eat来执行了。

6.4 消息重定向

在WYPerson类中不实现eat方法,并且消息接受者重定向方法中返回nil。我们在消息重定向中改变选择器指向或者消息接收者指向。

WYPerson的函数实现:

/*
 返回一个方法签名对象,表示这个函数的返回值类型和参数类型
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

//forwardInvoWYCation通知当前对象,并将NSInvoWYCation消息传递过来
/*
 有两个要点:
    1、决定消息接收者
    2、转发
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //1、消息接收者

    WYCat *cat = [[WYCat alloc] init];
    //anInvoWYCation表示消息,获取该消息的方法选择器
    if ([cat respondsToSelector:[anInvocation selector]]) {
        //重新指向接受者并发出消息
//        [anInvocation invokeWithTarget:cat];
        
        //重新指向消息接收者
//        anInvocation.target = cat;
        
        //重新指向选择器
        anInvocation.selector = @selector(forwardInvocationTest);
        //发出消息
        [anInvocation invoke];
    } else {
        [super forwardInvocation:anInvocation];
    }

}

- (void)forwardInvocationTest{
    NSLog(@"大家好,我是一个消息重定向的的实现函数,如果找不到eat函数,就会执行我");
}

运行结果:

2021-10-17 15:25:12.703920+0800 消息发送[86077:1569274] 大家好,我是一个消息重定向的的实现函数,如果找不到eat函数,就会执行我

注意:

  • 在消息接受者重新中更改消息,NSInvocation就代表消息,进入到该类中可以查看到我们可以更改哪些内容。
  • 经查看发现我们可以更改的消息内容只有target、selector两种,分别表示消息接收者、方法选择器。
  • NSMethodSignature是只读的,我们无法更改,必须在methodSignatureForSelector方法中设置。
@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end
  • 需要注意的是methodSignature必须要与函数的真实方法签名一致,否则不匹配将仍然找不到该函数

下面的代码经验证,确实仍然会提示找不到,因为不匹配

- (void)forwardInvocationTest:(NSString *)abc{
    NSLog(@"大家好,我是一个消息重定向的的实现函数,如果找不到eat函数,就会执行我");
}
  • methodSignatureForSelector是一定要写的,需要先设置方法签名

6.5 总结

  1. 消息转发的流程不属于消息发送,只是在消息发送失败后向消息接受者发送一个通知,我们在这个通知中改变消息的选择器或接受者,以此来达到消息的二次发送。
  2. 消息接收者重定向只能修改消息接受者,转发给另一个对象来执行同名函数
  3. 消息重定向可以修改消息接收者和方法选择器,也就是可以转发给另一个对象的某个方法
  4. 消息接收者重定向必须要先通过methodSignatureForSelector设置方法签名。

7、报错函数认识

消息发送流程的lookUpImpOrForward函数中,我们看到如果快速查找、慢速查找、动态方法解析均没有成功,则返回一个转发函数,该函数为_objc_msgForward_impcache。它就是用来报错的,因此我们就开始分析它,以此来查看消息发送失败后最后的操作。

7.1 找到报错函数

这里看到报错函数为_objc_msgForward_impcache

//方法转发(报错方法)
const IMP forward_imp = (IMP)_objc_msgForward_impcache;

7.2 汇编查找

  • 先全局搜索_objc_msgForward_impcache,发现在汇编中
  • 执行到__objc_msgForward
  • __objc_msgForward里执行的是__objc_forward_handler,所以接下来找__objc_forward_handler
汇编查找.png

7.3 __objc_forward_handler的查找

全局搜索,没有在汇编中查到,猜想可能是在C语言中,因此在源码中去掉一个下划线进行全局搜索_objc_forward_handler,发现最终的报错函数是objc_defaultForwardHandler。

查找到源码如下:

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

7.4 报错函数objc_defaultForwardHandler分析

查看报错函数的代码,发现就是我们平常执行方法时找不到函数所报的错,终于找到头了。消息发送结束。

objc_defaultForwardHandler源码

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

8、总结

我们从上层方法调用,通过Clang查看底层实现,发现底层是通过objc_msgSend进行消息发送。之后通过objc_msgSend逐步探索,在汇编中查找cache的查找过程,在lookUpImpOrForward查找方法列表和动态方法解析的过程,之后通过官方文档和打印日志发现了消息转发对消息的二次挽救,最后找到了我们常见的报错函数,至此我们调用一次方法所经历的所有内容宣告结束。

09-方法调用流程.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容