07、iOS底层探索-objc_msgSend
highlight: hybrid
上篇文章介绍了cache缓存的底层原理,知道cache是为了方法再次调用时能更快的被响应,这篇我们了解一下从cache缓存中读取方法
1、方法调用底层实现
1.1、转换cpp文件
-
想要了解方法调用的底层实现,我们可以实际调用方法,然后看底层的C++实现,转换指令为 :
clang -rewrite-objc xxx.m
-
生成cpp文件,因为方法调用写在了main函数里,所以在cpp文件中也查找main函数位置
1.2、objc_msgSend
- 图中我们可以看出,有参数的方法只有参数部分相对复杂, 无参数的实例方法play与类方法jump 在编译后底层调用
objc_msgSend
方法,默认有两个参数:- 第一个参数 :
消息的接受者
;实例方法为创建的具体person实例,类方法为 objc_getClass() 拿到的类 - 第二个参数 :
消息的方法名
;
- 第一个参数 :
- 我们可以直接将编译后的方法粘贴到代码中使用,(并且消息方法名
sel_registerName("jump")
也可以用NSSelectorFromString(@"jump")
替换)能成功调起jump方法中的打印,说明消息确实通过 objc_msgSend 发送成功
1.3、objc_msgSendSuper
-
我们再看一下常用的super类调用方法时的情况,对代码进行一些修改,在 init 方法中添加 self 调用与 super 调用
- 可以看出,打印出的类都是LZPerson,而非LZPerson继承的NSObject,这是为什么呢?我们还需要看一下cpp文件
-
因为写在 init 方法中,所以我们找 LZPerson 的 init 方法
- 通过比对可以发现,[super class]中的内容不是通过 objc_msgSend 发送了,而是变成了 objc_msgSendSuper ,对于其作用我们可以借助苹果文档查阅,方式为 :
菜单栏 --> Help --> Developer Documentation
对于其作用我们借助翻译工具查看大概 - 再来需要看参数含义 :
重点是说第一个参数是一个
objc_super数据结构的指针
,其中包括要接收消息的类的实例
和开始搜索方法实现的超类
- 如此一来cpp文件中的内容就清晰了,接收消息的类的实例与objc_msgSend一样还是self,只是是从Super类开始找起,所以都打印了LZPerson
- objc_super结构体 :
```swift /// Specifies the superclass of an instance. 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 */
};
endif
``` - 手动仿写,能够正常打印
- 通过比对可以发现,[super class]中的内容不是通过 objc_msgSend 发送了,而是变成了 objc_msgSendSuper ,对于其作用我们可以借助苹果文档查阅,方式为 :
2、objc_msgSend流程
- 每个架构下都有定义objc_msgSend,这里我们看arm64下的,使用了汇编代码,因为汇编代码效率更高执行更快
- 看懂汇编不是我们的目的,所以省略分析汇编过程,后边直接给出里边代码的功能流程
2.1、获取类对象
- 判断 receiver是否存在
- 通过 receiver 的 isa指针 查找class
2.2、方法的快速查找(cache中查找)
CacheLookup
-
找到类对象后,进入 CacheLookup 流程(汇编实现仅供了解) ```swift //在cache中通过sel查找imp的核心流程 .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant // // Restart protocol: // // As soon as we're past the LLookupStart\Function 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\Function, // then our PC will be reset to LLookupRecover\Function 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 //
//从x16中取出class移到x15中 mov x15, x16 // stash the original isa //开始查找 LLookupStart\Function: // p1 = SEL, p16 = isa
if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//ldr表示将一个值存入到p10寄存器中 //x16表示p16寄存器存储的值,当前是Class //#数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节 //#define CACHE (2 * SIZEOF_POINTER) //经计算,p10就是cache ldr p10, [x16, #CACHE] // p10 = mask|buckets lsr p11, p10, #48 // p11 = mask and p10, p10, #0xffffffffffff // p10 = buckets and w12, w1, w11 // x12 = _cmd & mask //真机64位看这个
elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *) ldr p11, [x16, #CACHE] // p11 = mask|buckets
if CONFIG_USE_PREOPT_CACHES
//获取buckets
if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function and p10, p11, #0x0000ffffffffffff // p10 = buckets
else
//and表示与运算,将与上mask后的buckets值保存到p10寄存器 and p10, p11, #0x0000fffffffffffe // p10 = buckets //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt tbnz p11, #0, LLookupPreopt\Function
endif
//按位右移7个单位,存到p12里面,p0是对象,p1是_cmd eor p12, p1, p1, LSR #7 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//LSR表示逻辑向右偏移 //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask //这个是哈希算法,p12存储的就是搜索下标(哈希地址) //整句表示_cmd & mask并保存到p12 and p12, p1, p11, LSR #48 // x12 = _cmd & mask
endif // CONFIG_USE_PREOPT_CACHES
elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets 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的内存平移 //PTRSHIFT经全局搜索发现是3 //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16 //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中 add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit cmp p9, p1 // if (sel != _cmd) { //b.ne表示如果不相同则跳转到2f b.ne 3f // scan more // } else { 2: CacheHit \Mode // hit: call or return imp // } //向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; //通过p13和p10来判断是否是第一个bucket cmp p13, p10 // } while (bucket >= buckets) b.hs 1b ```
-
CacheLookup 实现的功能
- class通过内存平移找到 cache
- 从cache 获取 bucket_t
- bucket_t 中比对 sel
- bucket_t中匹配到缓存的sel --> 调用cacheHit --> 调用对应imp
- bucket_t中未匹配到缓存的sel --> 调用_objc_msgSend_uncached
2.3、方法的慢速查找
- bucket_t中未能命中imp,调用
_objc_msgSend_uncached
- 执行 MethodTableLookup 方法
- 执行 _lookUpImpOrForward 方法,跳出汇编层面,全局搜索 lookUpImpOrForward 找其实现 开始在方法列表里找
-
再找一次cache,为的是防止多线程操作时,刚好调用函数
,还未找到的话调用 getMethodNoSuper_nolock,尝试在 methodList 中找 -
简单的跳转找方法 search_method_list_inline -->
findMethodInSortedMethodList
-
最终 findMethodInSortedMethodList 通过二分查找来找方法(分类方法放在MethodList前边位置)
methodList中找到方法
goto done
走下边 done 方法log_and_fill_cache
将方法缓存到 cache 中
methodList中未找到方法
- 如上边图示,
将当前查找类换为其父类,imp指针置为消息转发指针forward_imp
,容易被忘的点是这些查找方法是在for循环中,换句话说 换成父类后会重新再进行一遍相同的查找操作,直到最终扔未找到那么forward_imp指针不再被替换,转入消息转发流程
总结
- 方法调用流程 :
- _objc_msgSend_uncached --> MethodTableLookup --> lookUpImpOrForward --> cache_getImp(再找cache) / getMethodNoSuper_nolock --> search_method_list_inline --> findMethodInSortedMethodList
- 逻辑流程 :
- lookUpImpOrForward --> 先找当前类的methodList --> 找父类cache --> 找父类methodList --> 父类为nil --> forward_imp --> 消息转发