有过一些oc基础的开发对oc的消息机制应该都是不陌生的,调用某个对象的方法其实就是给对象发送一条消息,然后接收的对象根据接收到的方法,查找自己的方法列表,然后调用的过程。那oc是如何查找方法的?,是每一次都是去遍历自己的方法列表吗,那这样岂不是效率很低?,如何才能提高查找效率呢?我相信你也有这样的疑惑的,本篇将带来苹果的底层是如何实现的。
预备知识
开始之前你需要先知道以下几点
1. 什么是SEL
先看下下面的代码
1 | NSLog(@"----%p--%p---%p--%p",@selector(kk),@selector(kk),@selector(kk),@selector(kk)); |
发现输出的结果都是一样的
1 | ----0x100000f5f--0x100000f5f---0x100000f5f--0x100000f5f |
好像这样也不能说明SEL到底是什么,这个问题先放在这里,看完整个文章你就会明白到底是什么了。到目前为止,我们可以看出的是,同一个方法的SEL是一样的,转成(unsigned long)也是一样的,目前知道这么多已经足够了
2. 对象方法是放在类对象中的,类方法是放在元类对象中的。
不清楚的可以看下我的这篇介绍
3. 对象的内存结构
1 | struct objc_class : objc_object { |
4. 最好有些c++语法的基本认识,推荐这篇文章入门
5. 关于runtime源码,可以点击下载,可以直接运行
演示代码
1 | //Person.h |
由于我的mac是beta版本,源码跑不起来,但是不影响查看代码,下面struct相关的代码是我把runtime代码和本文相关的部分复制了一份下来,方便查看信息
1 | #import <Foundation/Foundation.h> |
正文
苹果对于方法的缓存方式是通过散列表实现的,所谓散列表就是通过一个函数,计算出某个元素应该放在哪个位置,或者从哪个位置取出的存储方式,从而提高查找速度。举个例子,现有一个长度为10的数组arr
,
1 | NSMutableArray *arr = [NSMutableArray arrayWithCapacity:10]; |
现在分别有1
、20
、3
、9
四个数字需要存放到数组中,这4个数字该放在哪个位置呢,我们定义一个函数,来计算4个数字应该存放的位置,这里我们简单的将数字的除以10的余数作为索引
1 | int indexWithEle(int ele) { |
计算的余数分别是
1 | 1 |
分别放到对应的位置那么arr
数组的元素分布如图
比如我们要9
,那也是通过indexWithEle
函数计算出索引为9
那么直接arr[9]直接就拿到元素了,就不用遍历数组了
苹果对于方法的缓存也是差不多的,一些小的差异后面会说到,方法的缓存信息都是放在cache
中的
1 |
|
cache_t
的结构如下
1 | struct bucket_t { |
_buckets
是存放的方法的信息;_mask
就相当于上面例子中的10
,这里取得是_buckets
的长度-1
_occupied
表示_buckets
拥有的元素的个数
cache_t
结构如图
刚才说到实现散列表缓存的话需要:
- 一个计算索引的函数;
- 一个集合
苹果计算索引的函数是将函数的SEL
的数值&
上_mask
的结果
1 | static inline mask_t cache_hash(cache_key_t key, mask_t mask) |
cache_key_t
其实就是unsigned long
上面讲到一个SEL
的数值是一样的mask
是_buckets
长度减1。
要知道一点就是c = a&b ,那么c应该是<= b的,所以mask
是_buckets
的长度减1,之前我们还有一个细节没考虑到那就是万一&出来的索引值已经有了怎么处理,苹果的__arm64__
实现是直接将索引-1
1 | bucket_t * cache_t::find(cache_key_t k, id receiver) |
如果索引相同则直接-1
1 |
|
函数已经有了那么集合呢看下面的方法
1 | static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) |
现在数组也有了,还有一个问题,比如一开始初始化的数组大小是4,当元素大于4的时候如何处理呢,苹果的处理是当元素的个数达到总数的3/4的时候就会扩容了
1 | void cache_t::expand() |
也就是按照4
、8
、16
…扩张下去,那么现在所有的东西都齐全了,以上说的这些都是理论,下面将会通过例子来一一验证正确性
将代码修改如下
我们来看看p
的test2
方法是放在哪里的,图中红色标出的地方,由于对象的方法是放在类对象中的,红色部分要写类对象,[p class]
、 [Person class]
、objc_getClass(p)
都是可以的。
1 | (lldb) p (unsigned long)@selector(test2) // test2的数值 |
上面我们通过自己计算得到了test2
位于_buckets
的第一个位置,也确实正确取出来了,有个地方需要注意一下,这里我们正好没有重复的索引,所以正好对了,当有重复的时候可能要-1或者–1 … 了
再修改代码如下
具体
发现此时的_mask
为7了,_occupied
为1
了,当
1 | p objP->cache._buckets[1] |
说明此时的缓存只有[p class]
了,这也正好验证了当超过3/4时会扩容,到62行的时候缓存的方法有init
、test2
、test1
,此时达到总数4
的3/43
所以扩容为4
* 2 = 8
好了到这里也就明白了方法缓存的过程了,对于什么是SEL
,相信也有认识了,其实就是一个编号,用来计算最终方法的存放位置的他的value就是函数的地址