Runtime之方法缓存
我们都知道,在Objective-c里面,调用一个方法,其实在runtime层的时候会翻译成
objc_msgSend(receiver, SEL)
可以想象一下,在继承关系中,一个比较深度的子类去调用父类的父类的父类... ...的方法的时候,如果没有缓存,每次都会用isa指针去挨个搜索,查找链是非常长的,如果类中的方法比较多,比较费时费力,可以看一个比较明显的例子:
1.png
在这种情况下,如果没有方法缓存,查找会变得非常耗时。
首先,先看看,方法缓存是放在哪个地方的,在类的定义中就有方法缓存,具体代码如下:
2.png
3.png
4.png
5.png
所以,是方法缓存是根据
类来的, 并不是根据具体的类的对象来的。
方法缓存的实现可以到runtime源码中去看,为了优化性能,objc_msgSend是用汇编来实现的,在objc-msg-arm.s文件中,具体实现的步骤是:
-
判断receiver是否是nil。
-
从缓存里面寻找SEL,找到就分发,否则3。
-
跳转到
_objc_msgSend_uncached,利用_class_lookupMethodAndLoadCache3方法(objc-class.mm中,具体可以看下面)寻找SEL。
对应的代码为:
6.png
7.png
从代码中可以看到, 如果没有找到方法缓存,就会跳转到\_objc\_msg\_uncached这里,里面有\_class\_lookupMethodAndLoadCache3
这个函数的具体的实现如下:
根据注释可以知道:此方法可以避免再去缓存查找方法,直接去方法列表去找。
8.png
9.png
其中:
-
mask: 表示当前缓存能达到的最大的size,从0开始,所以total = mask + 1 -
occupied: 表示占用的内存标志,顺便说一句,方法缓存是通过 “散列表” 的形式 实现的,散列表根据 哈希算法来定位位置,所以会产生空位,occupied用来表示已经使用的内存的个数 -
buckets:就是用数组来表示存储缓存的散列表的存储空间,其中的每一个Method类型表示一个可用的方法缓存。注意:其中结构体中,最后一个成员用[1], 说明中这是一个“可变数组”,在我以前接触到的c语言中,发现有的平台是用[0]来表示,有的是用[1]来表示可变数组
具体到Method的定义:
10.png
11.png
-
name: 表示被缓存的方法名字 -
types: 存储着方法的的参数类型和返回值类型 -
imp: 就是方法的具体实现
还有,往散列表中 存方法缓存 和 取方法缓存
- 存方法缓存是在objc-cache-old.mm文件中实现的,
12.png
13.png
这里就是往散列表中存储的具体实现,其中的散列查找算法是:
14.png
位置是通过sel指针偏移后和mask与后的结果得出
- 从缓存中取方法
取缓存的代码是 跟objc_msgSend的实现在同一个文件中,obj-msg-arm.s中,为了查找的性能优化,也是通过汇编来实现的,方法名字是 CacheLookup,具体实现是:
15.png
16.png
根据查阅其中的 汇编关键字 以及注释,可以知道,取缓存和加缓存的逻辑差不多,也是根据hash去定位,如果出现冲突,根据解决hash冲突规则,继续hash, 直到找到为止,这里是 ++ 的实现形式