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