《iOS内卷》:从源码角度分析objc_msgSend到底做了什么

298 阅读4分钟

image.png

一、前言

我们都知道iOS的方法调用其实就是消息发送,到最后都会调用objc_msgSend方法。这个方法的调用实在是太频繁,为了提高调用的性能,苹果将这个方法的实现用汇编语言来写。汇编更接近于底层的机器语言,执行性能比高级语言更好。

但正是这个缘故,很多人对于objc_msgSend的探索也止步于此:

image.png

望门兴叹😤😤😤

下面,我们从别人脚步停止的地方开始,从汇编源码的角度,窥探objc_msgSend到底做了什么。

二、objc_msgSend主要流程

我们以objc-msg-arm64.s为例,不同的架构流程有所不同。

1、通过对象的isa指针找到对应的类。
2、从类的cache_t中找到缓存数组buckets
3、计算哈希下标,这个下标用来访问buckets数组元素。之前插入方法缓存到buckets中时需要计算哈希下标,现在访问时也需要。
4、比较目标selbucket中的sel,判断是否为我们需要的bucket
5、找到目标bucket后,提取imp进行调用。

image.png

💡 如何快速读懂整个流程

这里整理了整个流程下来会经常使用的寄存器,以及寄存器里存放的数据。这些寄存器里存放的数据基本不会改变:

X0:receiver,objc_msgSend函数的调用者
X1:sel
X10:buckets数组
X11:mask | buckets
X12:哈希下标
X13:遍历到的bucket所在位置
X16:isa/class
X17:imp

如果你读源码到某个位置懵了,可以回来这里整理一下思路😑

1、GetClassFromIsa_p16

根据isa,获取对应的class,并放到p16寄存器里。

函数入参
src:p13(isa)
needs_auth:1
auth_address:x0

image.png

① ExtractISA

这一步是真正去获取class的操作。isa & ISA_MASK之后,将结果(class)存在p16寄存器。

ExtractISA 入参
$0:p16
$1:isa

image.png

2、CacheLookup

根据sel,从对应的类中去寻找缓存bucket

image.png

函数入参
Mode:NORMAL
Function:_objc_msgSend
MissLabelDynamic:__objc_msgSend_uncached
MissLabelConstant:nil

这个方法相对来说比较长,且都是汇编。汇编确实晦涩难懂,但好在苹果还是比较贴心,很多地方都给出了注释。只要你懂得缓存是怎么写入的,那你一定能看懂下面的代码,缓存的读取其实就是反过来的过程。

我们将它分割成四个部分:

① 计算buckets数组的位置和哈希下标

我们首先要找到buckets数组的位置,以及根据sel算出缓存所在的下标,才能从buckets数组中找到对应的缓存。

image.png

② 在左半部分寻找缓存bucket

从哈希下标内存位置向前遍历,遍历到buckets 0号内存位置为止。

这时候我们已经找到了哈希下标,但是当初插入方法缓存时,我们有处理哈希冲突的情况,实际插入的位置可能有偏移。现在确定的哈希下标位置可能不是当初真正的插入点,所以我们需要将sel和找到的bucket中的sel做比较,如果不一致,就继续往左边的位置遍历寻找,直到遍历到buckets数组的0号位置。

在这个过程中,如果sel等于buckct中的sel,则调用CacheHit函数。如果直到bucket中sel等于0时,依然没有找到,则说明已经到了缓存的末尾了,因为插入的时候是向左遍历插入的。之后会调用MissLabelDynamic对应的方法。

image.png

③ 获得buckets数组的最后位置

如果从哈希下标开始找,直到buckets数组的0号位置(左半部分)都找不到对应的bucket,我们就需要找一下buckets的右半部分了。我们先找到buckets数组的最后位置(内存位置,非下标),再往前遍历。

image.png

④ 在右半部分寻找缓存bucket

buckets最后的内存位置,向前遍历,直到遍历到哈希下标所在的内存位置为止。

image.png

3、CacheHit

缓存命中,找到sel对应的缓存bucket时会调用。在这个方法里,一般来说会直接调用找到的imp

image.png

缓存查找分为3个模式:

1、normal:最普遍的情况。我们调用某个方法时,先查找有没有对应的方法缓存,有的话就调用,没有的话再去类方法列表继续查找。
2、getImp:单纯的获取方法缓存,不会执行方法。
3、lookup:这个暂时不清楚使用场景。按字面意思,也许只是找一下有没有这个方法缓存?

三、总结

本节梳理了objc_msgSend底层实现的主要流程。其他的一些异常处理流程,包括没找到方法缓存时应该如何处理等,就属于慢速查找流程、动态方法协议之类的知识了。这个后续再慢慢梳理。

不得不说,梳理底层基础知识真的是个漫长的过程。😂

路漫漫其修远兮,吾将上下而求索。