一:猜想与运行结果验证
前言:我们知道一个oc对象,在底层都会被编译成一个c++结构体,部分代码如下,这里不再讨论结构体的关系,只列出部分关键源码
struct objc_class;
struct objc_object;
struct objc_object {
private:
isa_t isa;
}
typedef struct objc_class *Class;
typedef struct objc_object *id;
//注:每个类对象都是该类型的结构体变量,cache就是缓存的方法,bits里存储着该类的所有实例对象方法
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//其它声明的函数已做删减
}
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
问题引入:但每个对象调用对象方法(这里只探究实例对象方法,类方法和对象方法类似),到底是怎么去查找的?
问题简答:我们每个实例对象调用方法时,分为以下几种情况:
1.首先去类对象的缓存cache中去查找方法,如果查找到该方法则直接调用
2.如果在类对象中未找到方法,则去类对象的方法列表寻找方法,如果找到方法,则调用该方法,同时缓存一份到cache中
3.如果在类对象的cache和方法列表中都没有找到该方法,则通过类对象的superClass指针到父类的类对象的cache中查
找,如果找到,则调用该方法,同时缓存一份到自身的类对象的cache中
4.如果在自身的类对象的cache中,方法列表中,父类的cache中都没找到,则到父类的方法列表中查找,如果找到,则调用该
方法,同时缓存一 份到父类类对象的cache中,也缓存一份到自己类对象的cache中.
5.如果在父类的方法列表里也找不到该方法,则重复执行4,层层向上查找,直到找到NSObject,如果NSObject都没有,那
查找过程就结束,报错
验证:我们声明两个类,继承关系:EZStudent-->EZPerson-->NSObject
注意:查看缓存的方法,首先我们需要拿到类对象的cache缓存,打印缓存列表即可.因为OC对象在底层都会被编译成结构体,
所以我们自己构造和底层类型一模一样的结构体,然后强制转换为我们的结构体类型的变量,就能打印出实际运行过程
中每个结构体成员的值.我们这里的目的是拿到objc_class结构体变量中的cache成员.
struct my_buckets_t{
my_cache_key_t _key;
IMP _imp;
};
//cache在底层也是个结构体
struct my_cache_t{
struct my_buckets_t *_buckets;//缓存的方法数组
my_mask_t _mask;//散列表长度减一,和SEL做相与操作可得到散列表索引值,方知该方法缓存到散列表的哪个位置.
my_mask_t _occupied; //已缓存的方法数量
//用来获取缓存方法的key,imp等值
IMP imp(SEL selector){
my_mask_t begin = _mask & (long long)selector;
my_mask_t i = begin;
do {
if (_buckets[i]._key == 0 || _buckets[i]._key == (long long)selector) {
return _buckets[i]._imp;
}
} while ((i = cache_next(i, _mask)) != begin);
return NULL;
}
};
struct my_objc_object{
Class isa;
};
struct my_objc_class : my_objc_object{
Class superclass;
struct my_cache_t cache;
...//其它信息省略,因为这里不研究其它成员的信息
};
从结果可以看出我们上面的猜想.
另外,散列表会根据实际情况,进行扩容.按源码的规定,当当前即将调用增加缓存的方法时,如果超过散列表长度的3/4,就会扩容,扩容是将当前散列表长度进行*2,但有上限,超过上限不允许再扩容,初始散列表长度为4(这些规定在源码中都能找到,下面我们会一一解读源码),现在看实际运行结果,当我们调用了init,studentRun
从上图可以看出,init方法的key值为100509109,转成十六进制就是5fda5b5,和值为3的mask相与结果为1,即得到散列表的索引值.最终key为100509109,地址为0x101bcee2f名为init的方法就缓存在散列表索引为1的位置里.
从运行结果看,也符合我们的猜想.下面就是从源码角度来再次印证猜想.
二.源码解读
1.解读思路:
需要了解的几点:源码从哪里开始看?看哪些方法?
a>我们需要解读cache的源码,就要知道cache是存在哪里的,我们知道cache是objc_class结构体的一个成员.首先就得看objc_class结构体,如图:
b>要研究cache就得进cache查看cache到底是什么,如图:
其中typedef uintptr_t cache_key_t;
这里有一点要说的是,Objective-C 数据结构中,存在一个 name - selector 的映射表如图:
12(方法对应的key) -->addObject:
755 -->setEntryDate
332 -->count
c>从a和b两个步骤中我们已经看到cache,散列表buckets,需要缓存的方法地址imp,需要缓存的方法的key的对应关系.现在就看程
序运行过程中方法的调用顺序.因为实例对象调用实例对象方法是从类对象的cache中查找方法,如果查找不到,则到类对象的方
法列表里继续查找.会进入cache_fill_nolock()函数开始查找方法,根据查找结果做相应处理.现在看看这个函数的实现:
截图中注释已经很详细了,就不再说了,现在需要查看:第一点:expand()函数是怎么对散列表进行扩容的,第二点:find()函数是怎么在散列表中寻找到合适位置存放本次调用的方法的,第三点:set()函数是怎样将方法地址imp和key存到cache里的
d>首先看expand()函数,代码截图和注释如下:
其中的INIT_CACHE_SIZE源码截图如下:
e>现在再来看find()函数是怎么查找到合适的空位,用来存储新的方法的,代码截图和注释如下:
f>继续看set()函数的细节,代码截图和注释如下:
在这之前,有一句代码
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
表示,如果当前位置的key为0,这个位置就没有存储过方法,所以要对结构体的occupied成员加1操作,如果不为0,则表示散列表存储过这个方法,set函数会对已经存储过的方法做单独处理
g>reallocate函数细节,初始化散列表和散列表扩容时都会调用该函数,代码截图注释如下: