1. Class
对象的结构class_rw_t
通过前面对isa
本质结构有了新的认识,今天来回顾Class
对象的结构,重新认识Class
的内部结构。
首先来看一下Class
的内部结构代码,对探寻Class
的本质做简单回顾。
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();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
}
通过位运算&
得到class_rw_t
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
1.1 方法/属性/协议的存储class_rw_t
上述源码中我们知道bits & FAST_DATA_MASK
位运算之后,可以得到class_rw_t
,而class_rw_t
中存储着方法列表、属性列表以及协议列表,来看一下class_rw_t
部分源码:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
上述源码中,method_array_t、property_array_t、protocol_array_t
其实都是二维数组,来到method_array_t、property_array_t、protocol_array_t
内部看一下。这里以method_array_t
为例,method_array_t
本身就是一个数组,数组里面存放的是数组method_list_t
,method_list_t
里面最终存放的是method_t
:
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls);
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
class property_array_t :
public list_array_tt<property_t, property_list_t>
{
typedef list_array_tt<property_t, property_list_t> Super;
public:
property_array_t duplicate() {
return Super::duplicate<property_array_t>();
}
};
class protocol_array_t :
public list_array_tt<protocol_ref_t, protocol_list_t>
{
typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;
public:
protocol_array_t duplicate() {
return Super::duplicate<protocol_array_t>();
}
};
class_rw_t
里面的methods、properties、protocols
是二维数组,是可读可写的,其中包含了类的初始内容以及分类的内容。
这里以method_array_t
为例,图示其中的结构。
[图片上传失败...(image-98fd3a-1590460455948)]
class_ro_t
我们之前提到过class_ro_t
中也有存储方法、属性、协议列表,另外还有成员变量列表。
接着来看一下class_ro_t
部分代码:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
上述源码class_rw_t
中可以看到其成员const class_ro_t *ro
是只读的,class_ro_t
内部直接存储的就是method_list_t、protocol_list_t 、property_list_t
类型的一维数组,数组里面分别存放的是类的初始信息,以method_list_t
为例,method_list_t
中直接存放的就是method_t
,但是是只读的,不允许增加删除修改。
以方法列表为例,class_rw_t中
的成员methods
是二维数组的结构,并且可读可写,因此可以动态的添加方法,并且更加便于分类方法的添加。因为我们在Category
的本质里面提到过,attachList
函数内通过memmove
和memcpy
两个操作将分类的方法列表合并在本类的方法列表中。那么此时就将分类的方法和本类的方法统一整合到一起了。
其实一开始类的方法,属性,成员变量属性协议等等都是存放在class_ro_t
中的,当程序运行的时候,需要将分类中的列表跟类初始的列表合并在一起的时,就会将class_ro_t
中的列表和分类中的列表合并起来存放在class_rw_t
中,也就是说class_rw_t
中有部分列表是从class_ro_t
里面拿出来的。并且最终和分类的方法合并。可以通过runtime
的源码可以看出这一点。
realizeClass
部分源码
static Class realizeClassWithoutSwift(Class cls)
{
runtimeLock.assertLocked();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// 最开始cls->data是指向ro的
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {// rw已经初始化并且分配内存空间
rw = cls->data(); // cls->data指向rw
ro = cls->data()->ro; // cls->data()->ro指向ro
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {// 如果rw并不存在,则为rw分配空间
// 分配空间
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
// rw->ro重新指向ro
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
// 将rw传入setData函数,等于cls->data()重新指向rw
cls->setData(rw);
}
}
那么从上述源码中就可以发现,类的初始信息本来其实是存储在class_ro_t
中的,并且最开始cls->data()
指向class_ro_t
的,也就是说最开始bits.data()
得到的是ro
,但是在运行过程中创建了class_rw_t
,并将cls->data
指向rw
,同时将初始信息ro
赋值给rw
中的ro
。最后在通过setData(rw)
设置data
。那么此时bits.data()
得到的就是rw
,之后再去检查是否有分类,同时将分类的方法,属性,协议列表整合存储在class_rw_t
中的方法,属性及协议列表中。
通过上述对源码的分析,我们对class_rw_t
内部存储方法、属性、协议列表的过程有了更清晰的认识,那么接下来探寻class_rw_t
中是如何存储方法的。
2. class_rw_t
内部是如何存储方法的
2.1 method_t
结构体
我们知道method_array_t、property_array_t、protocol_array_t
中以method_array_t
为例,method_array_t
中最终存储的是method_t
,method_t
是对方法、函数的封装,每一个方法对象就是一个method_t
。通过源码看一下method_t
的结构体:
// 部分源码
struct method_t {
SEL name; // 函数名
const char *types; // 编码(返回值类型,参数类型)
IMP imp; // 指向函数的指针(函数地址)
};
method_t
结构体中可以看到三个成员变量,我们依次来看三个成员变量分别代表什么。
method_t
成员1:SEL
SEL
代表方法/函数名,一般叫做选择器,底层结构字符串char *
类似typedef struct objc_selector *SEL;
,可以把SEL
看做是方法名字符串。
SEL
可以通过@selector()
和sel_registerName()获得
:
SEL sel1 = @selector(test);
SEL sel2 = sel_registerName("test");
NSLog(@"%p,%p", sel1,sel2);
也可以通过sel_getName()
和NSStringFromSelector()
将SEL
转成字符串:
char *string = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel2);
不同类中相同名字的方法,所对应的方法选择器是相同的。
2020-01-17 15:06:36.963826+0800 Runtime的本质2[13284:7389948] 0x7fff52516190,0x7fff52516190
SEL
仅仅代表方法的名字,并且不同类中相同的方法名的SEL
是全局唯一的。
method_t
成员2:types
types
包含了函数返回值,参数编码的字符串。通过字符串拼接的方式将返回值和参数拼接成一个字符串,来代表函数返回值及参数。
我们通过代码查看一下types
是如何代表函数返回值及参数的,首先通过自己模拟Class
的内部实现,通过强制转化来探寻内部数据:
Person *person = [[Person alloc] init];
xx_objc_class *cls = (__bridge xx_objc_class *)[Person class];
class_rw_t *data = cls->data();
NSLog(@"\n");
上图中可以看出types
的值为v16@0:8
,那么这个值代表什么呢?
Apple
为了能够清晰的使用字符串表示方法及其返回值,制定了一系列对应规则,通过下表可以看到一一对应关系
将types
的值同表中的一一对照查看types
的值v16@0:8
代表什么
- (void) test;
v 16 @ 0 : 8
void id SEL
// v 16: v代表函数返回值类型为void 16表示所有的参数的占用空间大小。
// @ 0: @代表第一个参数类型是id,0表示从0位开始存储,id占8字节空间。
// : 8: :表示第二个参数类型是SEL,8表示从第8位开始存储,SEL同样占8字节空间。
我们知道任何方法都默认有两个参数的,id
类型的self
,和SEL
类型的_cmd
,而上述通过对types
的分析同时也验证了这个说法。
为了能够看的更加清晰,我们为test
添加返回值及参数之后重新查看types
的值:
- (int)testWithAge:(int)age Height:(float)height;
同样通过上表找出一一对应的值,查看types
的值代表的方法
- (int)testWithAge:(int)age Height:(float)height
{
return 0;
}
i 24 @ 0 : 8 i 16 f 20
int id SEL int float
// 参数的总占用空间为 8 + 8 + 4 + 4 = 24
// 参数1:id 从第0位开始占据8字节空间
// 参数2:SEL 从第8位开始占据8字节空间
// 参数3:int 从第16位开始占据4字节空间
// 参数4:float 从第20位开始占据4字节空间
iOS
提供了@encode
的指令,可以将具体的类型转化成字符串编码:
NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));
输出:
2020-01-17 15:44:41.845060+0800 Runtime的本质2[15173:7428744] i
2020-01-17 15:44:41.845138+0800 Runtime的本质2[15173:7428744] f
2020-01-17 15:44:41.845211+0800 Runtime的本质2[15173:7428744] @
2020-01-17 15:44:41.845296+0800 Runtime的本质2[15173:7428744] :
上述代码中可以看到,对应关系确实如上表所示。
method_t
成员3:IMP
IMP
代表函数的具体实现,存储的内容是函数地址。也就是说当找到IMP
的时候就可以找到函数实现,进而对函数进行调用。
在上述代码中打印IMP
的值:
(lldb) p/x data->methods->first.imp
(IMP) $0 = 0x0000000100283f10 (Runtime的本质2`::-[Person testWithAge:Height:](int, float) at main.mm:24)
(lldb)
之后在test
方法内部打断点,并来到其方法内部(Debug -> Debug Workflow -> Always Show Disassembly
)可以看出imp中的存储的地址也就是方法实现的地址。
[图片上传失败...(image-c6b37a-1590460455952)]
2.2 总结
通过上面的学习我们知道了方法列表是如何存储在Class
类对象中的,但是当多次继承的子类想要调用基类方法时,就需要通过superclass
指针一层一层找到基类,在从基类方法列表中找到对应的方法进行调用。如果多次调用基类方法,那么就需要多次遍历每一层父类的方法列表,这对性能来说无疑是伤害巨大的。
Apple
通过方法缓存的形式解决了这一问题,接下来我们来探寻Class
类对象是如何进行方法缓存的。
3. 方法缓存cache_t
原理
回到类对象结构体,成员变量cache
就是用来对方法进行缓存的。
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();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
}
cache_t cache:用来缓存曾经调用过的方法,可以提高方法的查找速度。
回顾方法调用过程:调用方法的时候,需要去方法列表里面进行遍历查找。如果方法不在列表里面,就会通过superclass
找到父类的类对象,在去父类类对象方法列表里面遍历查找。
如果方法需要调用很多次的话,那就相当于每次调用都需要去遍历多次方法列表,为了能够快速查找方法,Apple
设计了cache_t
来进行方法缓存。
每当调用方法的时候,会先去cache
中查找是否有缓存的方法,如果没有缓存,在去类对象方法列表中查找,以此类推直到找到方法之后,就会将方法直接存储在cache
中,下一次在调用这个方法的时候,就会在类对象的cache
里面找到这个方法,直接调用了。
3.1 cache_t
是如何进行缓存的
那么cache_t
是如何对方法进行缓存的呢?首先来看一下cache_t
的内部结构:
// 部分源码
struct cache_t {
struct bucket_t *_buckets; // 散列表 数组
mask_t _mask; // 散列表的长度-1,可以理解为最大的数组索引
mask_t _occupied; // 已经缓存的方法数量
};
bucket_t
是以数组的方式存储方法列表的,看一下bucket_t
内部结构:
// 部分源码
struct bucket_t {
private:
cache_key_t _key; // SEL作为Key
IMP _imp; // 函数的内存地址
};
从源码中可以看出bucket_t
中存储着SEL
和_imp
,通过key->value
的形式,以SEL
为key
,函数实现的内存地址_imp
为value
来存储方法。
通过一张图来展示一下cache_t
的结构。
[图片上传失败...(image-b13a55-1590460455952)]
上述bucket_t
列表我们称之为散列表(哈希表)
,散列表(Hash table
,也叫哈希表),是根据关键码值(Key value)
而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
散列表的原理
散列表的原理其实就是根据一个值(key
值)生成一个索引值(散列函数),然后通过这个索引值实现了表的存储和查询操作,而不用去遍历这个表,这样大大的提高了效率。但是会牺牲一定的内存空间。
那么Apple
如何在散列表中快速并且准确的找到对应的key
以及函数实现呢?这就需要我们通过源码来看一下Apple
的散列函数是如何设计的。
3.2 底层源码
首先来看一下存储函数的源码,主要查看几个函数,关键代码都有注释,不在赘述。
cache_fill()
及cache_fill_nolock()
函数
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// 如果没有initialize,直接return
if (!cls->isInitialized()) return;
// 确保线程安全,没有其他线程添加缓存
if (cache_getImp(cls, sel)) return;
// 通过类对象获取到cache
cache_t *cache = getCache(cls);
// 占用空间+1
mask_t newOccupied = cache->occupied() + 1;
// 获取缓存列表的缓存能力,能存储多少个键值对
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// 如果为空的,则创建空间,这里创建的空间为4个。
// 调用了reallocate函数
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// 如果所占用的空间占总数的3/4以下,则继续使用现在的空间
}
else {
// 如果占用空间超过3/4则扩展空间
// 调用了expand函数
cache->expand();
}
// 通过key查找合适的存储空间,key为sel
// 调用了find函数
bucket_t *bucket = cache->find(sel, receiver);
// 如果key==0则说明之前未存储过这个key,占用空间+1
if (bucket->sel() == 0) cache->incrementOccupied();
// 存储key,imp
bucket->set<Atomic>(sel, imp);
}
reallocate()
函数
通过上述源码看到reallocate函数负责分配散列表空间,来到reallocate函数内部。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 旧的散列表能否被释放
bool freeOld = canBeFreed();
// 获取旧的散列表
bucket_t *oldBuckets = buckets();
// 通过新的空间需求量创建新的散列表
bucket_t *newBuckets = allocateBuckets(newCapacity);
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
// 设置Buckets和Mash,Mask的值为散列表长度-1
setBucketsAndMask(newBuckets, newCapacity - 1);
// 释放旧的散列表
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
上述源码中首次传入reallocate函数的newCapacity为IINIT_CACHE_SIZE
,INIT_CACHE_SIZE
是个枚举值,也就是4。因此散列表最初创建的空间就是4个。
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
expand()
函数
当散列表的空间被占用超过3/4的时候,散列表会调用expand ()函数进行扩展,我们来看一下expand ()函数内散列表如何进行扩展的。
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
// 获取旧的散列表的存储空间
uint32_t oldCapacity = capacity();
// 将旧的散列表存储空间扩容至2倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
// 为新的存储空间赋值
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
// 调用reallocate函数,重新创建存储空间
reallocate(oldCapacity, newCapacity);
}
上述源码中可以发现散列表进行扩容时会将容量增至之前的2倍。
find()
函数
最后来看一下散列表中如何快速的通过key找到相应的bucket呢?我们来到find函数内部
bucket_t * cache_t::find(SEL s, id receiver)
{
assert(s != 0);
// 获取散列表
bucket_t *b = buckets();
// 获取mask
mask_t m = mask();
// 通过key找到key在散列表中存储的下标
mask_t begin = cache_hash(s, m);
// 将下标赋值给i
mask_t i = begin;
// 如果下标i中存储的bucket的key==0说明当前没有存储相应的key,将b[i]返回出去进行存储
// 如果下标i中存储的bucket的key==s,说明当前空间内已经存储了相应key,将b[i]返回出去进行存储
do {
if (b[i].sel() == 0 || b[i].sel() == s) {
// 如果满足条件则直接返回
return &b[i];
}
// 如果走到这里说明上面不满足,那么会往前移动一个空间重新进行判定,直到可以成功return为止
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)s, cls);
}
函数cache_hash (s, m)
用来通过key找到方法在散列表中存储的下标,来到cache_hash (s, m)
函数内部
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
可以发现cache_hash (s, m)
函数内部仅仅是进行了sel & mask
的按位与运算,得到下标即存储在相应的位置上。按位与运算在上文中已详细讲解过,这里不在赘述。
_mask
通过上面的分析我们知道_mask
的值是散列表的长度减一,那么任何数通过与_mask
进行按位与运算之后获得的值都会小于等于_mask
,因此不会出现数组溢出的情况。
举个例子,假设散列表的长度为8,那么mask的值为7
0101 1011 // 任意值
& 0000 0111 // mask = 7
------------
0000 0011 //获取的值始终等于或小于mask的值
3.3 总结
缓存过程
当第一次调用方法时,消息机制通过isa
找到方法之后,会对方法以SEL
为key
,IMP
为value
包装成一个bucket_t
,缓存在cache
的_buckets
数组中。
当第一次调用方法的时候,会创建长度为4的散列表,并将_mask
的值置为散列表的长度减一,之后通过位运算SEL & mask
计算出方法存储的索引值,并将方法缓存在散列表中。例如,如果计算出下标值为3,那么就将方法直接存储在下标为3的空间中,前面的空间会留空。
当散列表中存储的方法占据散列表长度超过3/4的时候,散列表会进行扩容操作,会创建一个新的散列表并且空间扩容至原来空间的2倍,并重置_mask
的值,并且会将原来已经缓存的bucket_t
重新计算新的索引值,按照新的索引值进行存储,最后释放旧的散列表。此时再有方法要进行缓存的话,就需要重新通过SEL & mask
计算出下标值之后在按照索引进行存储了。
如果一个类中方法很多,其中很可能会出现多个方法的SEL & mask
得到的值为同一个索引值,那么会调用cache_next
函数往索引值-1的位值去进行存储,如果索引值-1位空间中有存储方法,并且key
不与要存储的key
相同,那么再到前面一位进行比较,直到找到一位空间没有存储方法或者key
与要存储的key
相同为止,如果到索引为0位置的话,就会到下标为_mask
的空间,也就是最大索引处进行重新存储。
查找过程
当要查找方法时,并不需要遍历散列表,同样通过SEL & mask
计算出索引值,直接去索引值的空间取值即可,同上,如果索引值中存储的key
与要查找的key
不相同,就去前面一位查找。这样虽然占用了较多的内存空间,但是大大节省了时间,提高了查找的效率,也就是说其实Apple
是使用空间换取了存取的时间。
通过一张图更清晰的看一下其中的流程。
[图片上传失败...(image-9b0e77-1590460455952)]
4. 验证方法的缓存流程
通过一段代码演示一下 。同样使用仿照objc_class
结构体自定义一个结构体,并进行强制转化来查看其内部数据,自定义结构体在之前的文章中使用过多次这里不在赘述。
我们创建Person
类继承NSObject
,Student
类继承Person
,CollegeStudent
继承Student
。三个类分别有personTest
,studentTest
,colleaeStudentTest
方法
通过打印断点来看一下方法缓存的过程
CollegeStudent *collegeStudent = [[CollegeStudent alloc] init];
xx_objc_class *collegeStudentClass = (__bridge xx_objc_class *)[CollegeStudent class];
cache_t cache = collegeStudentClass->cache;
bucket_t *buckets = cache._buckets;
[collegeStudent personTest];
[collegeStudent studentTest];
NSLog(@"----------------------------");
for (int i = 0; i <= cache._mask; i++) {
bucket_t bucket = buckets[i];
NSLog(@"%s %p", bucket._key, bucket._imp);
}
NSLog(@"----------------------------");
[collegeStudent colleaeStudentTest];
cache = collegeStudentClass->cache;
buckets = cache._buckets;
NSLog(@"----------------------------");
for (int i = 0; i <= cache._mask; i++) {
bucket_t bucket = buckets[i];
NSLog(@"%s %p", bucket._key, bucket._imp);
}
NSLog(@"----------------------------");
NSLog(@"%p",@selector(colleaeStudentTest));
NSLog(@"----------------------------");
我们分别在collegeStudent
实例对象调用personTest
,studentTest
,colleaeStudentTest
方法处打断点查看cache
的变化。
personTest方法调用之前
从上图中可以发现,personTest
方法调用之前,cache
中有可能仅仅存储了init
方法,上图中可以看出init
方法恰好存储在下标为0的位置,因此我们可以看到,_mask
的值为3,验证我们上述源码中提到的散列表第一次存储时会分配的散列表的长度是4,_occupied
的值为1证明此时_buckets
中仅仅缓存了1个方法。
当collegeStudent
在调用personTest
的时候,首先发现collegeStudent
类对象的cache
中没有personTest
方法,就会去collegeStudent
类对象的方法列表中查找,方法列表中也没有,那么就通过superclass
指针找到Student
类对象,Studeng
类对象中cache
和方法列表同样没有,再通过superclass
指针找到Person
类对象,最终在Person
类对象方法列表中找到之后进行调用,并缓存在自己的collegeStudent
类对象的cache
中。
执行personTest
方法之后查看cache
方法的变化
上图中可以发现_occupied
值为2,说明此时personTest
方法已经被缓存在collegeStudent
类对象的cache
中。
同理执行过studentTest
方法之后,我们通过打印查看一下此时cache
内存储的信息
上图中可以看到cache
中确实存储了init 、personTest 、studentTest
三个方法。
那么执行过colleaeStudentTest
方法之后,此时cache
中应该对colleaeStudentTest
方法进行缓存。上面源码提到过,当存储的方法数超过散列表长度的3/4时,系统会重新创建一个容量为原来2倍的新的散列表替代原来的散列表。过掉colleaeStudentTest
方法,重新打印cache
内存储的方法查看。
可以看出上图中_bucket
散列表扩容之后仅仅存储了colleaeStudentTest
方法,并且上图中打印SEL & _mask
位运算得出索引的值5确实是_bucket
列表中colleaeStudentTest
方法存储的位置。
至此已经对Class
的结构及方法缓存的过程有了新的认知,Apple
通过散列表的形式对方法进行缓存,以少量的空间节省了大量查找方法的时间。