通过这张图描述了实例对象,类,元类之间的关系 ; 图中实线是 super_class
指针,虚线是 isa
指针。 从图中看出:
当发送一个实例方法的消息时,isa
指针会在这个类的实例方法列表中查找;
当发送一个类方法的消息时,isa
指针会在这个类的 meta-class
的方法列表中查找,meta-class
之所以重要,是因为它存储着一个类的所有类方法。
每个类都会有一个单独的 meta-class
,因为每个类的类方法基本不可能完全相同。
NSObject 1 2 3 4 5 6 7 @interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isa OBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop } typedef struct objc_class *Class;
万物皆对象,通过源码分析,NSObject
里面包含一个 objc_class
结构体类型的 isa
指针;
objc_class
通过源码分析:objc_class
里面包含一个 objc_class
结构体类型的 superclass
指针,可以通过 superclass
指针,查找到父类;cache_t
结构体的 cache
指针,cache
主要用于方法性能优化,对使用过的方法进行缓存,便于第二次查找;class_data_bits_t
结构体的 bits
指针,只含有一个 64
位的 bits
用于存储与类有关的信息;
在 objc_class
结构体中的注释写到 class_data_bits_t
相当于 class_rw_t
指针加上 rr/alloc
的标志。
通过将 bits
与 FAST_DATA_MASK
进行位运算,返回 class_rw_t *
指针,其中 Objc
的类的属性、方法、以及遵循的协议都放在 class_rw_t
结构体中;
cache_t 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; public: struct bucket_t *buckets(); mask_t mask(); mask_t occupied(); void incrementOccupied(); void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask); void initializeToEmpty(); mask_t capacity(); bool isConstantEmptyCache(); bool canBeFreed(); static size_t bytesForCapacity(uint32_t cap); static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap); void expand(); void reallocate(mask_t oldCapacity, mask_t newCapacity); struct bucket_t * find(cache_key_t key, id receiver); static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn)); }; struct bucket_t { private: cache_key_t _key; IMP _imp; public: inline cache_key_t key() const { return _key; } inline IMP imp() const { return (IMP)_imp; } inline void setKey(cache_key_t newKey) { _key = newKey; } inline void setImp(IMP newImp) { _imp = newImp; } void set(cache_key_t newKey, IMP newImp); };
cache
主要是为了优化方法调用的性能,一个接收者对象接收到一个消息时,它会根据 isa 指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是 methodLists
中遍历一遍,性能势必很差。这时,cache
就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到 cache
列表中,下次调用的时候就会优先去 cache
中查找,才去 methodLists
中查找方法。
从源码中可以看出,_buckets
指针是一个指向 bucket_t
结构体的哈希表,_buckets
哈希表里面包含多个 bucket_t
,每个 bucket_t
里面存放着 SEL
和 imp
函数的内存地址的对应关系;_mask
是一个 uint32_t
的指针,表示整个 _buckets
哈希表的长度;_occupied
也是一个 uint32_t
的指针,在 _buckets
哈希表中已经缓存的方法数量;
在 bucket_t
结构体中,_key
是一个 unsigned long
的指针,其实是一个被 hash
化的一串数值,就是方法的 sel
,也就是方法名;_imp
指针保持着对应的函数地址;
cache_t 如何缓存 sel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 void cache_fill(Class cls, SEL sel, IMP imp, id receiver) { cache_fill_nolock(cls, sel, imp, receiver); } static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { // Never cache before +initialize is done // 系统要求在类初始化完成之前,不能进行方法返回,因此如果类没有完成初始化就 return if (!cls->isInitialized()) return; // Make sure the entry wasn't added to the cache by some other thread // before we grabbed the cacheUpdateLock. // 因为有可能其他线程已经把该方法缓存起来,如果缓存中已经缓存过了,不用再缓存,直接 return if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); cache_key_t key = getKey(sel); // Use the cache as-is if it is less than 3/4 full mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } else { // Cache is too full. Expand it. cache->expand(); } // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); } void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; if ((uint32_t)(mask_t)newCapacity != newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); bucket_t *oldBuckets = buckets(); bucket_t *newBuckets = allocateBuckets(newCapacity); // Cache's old contents are not propagated. // This is thought to save cache memory at the cost of extra cache fills. // fixme re-measure this assert(newCapacity > 0); assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } }
先看缓存中是否已经存在了该方法,如果已经存在,直接return掉;
如果缓存是只读的,则需要重新申请缓存空间;
如果存入缓存后的大小小于当前大小的 3/4
,则当前缓存大小还可以使用,无需扩容;
如果缓存太满,需要扩容,扩容为原来大小的 2
倍,重新申请缓存空间;1 2 3 4 enum { INIT_CACHE_SIZE_LOG2 = 2, INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) };
如果是首次调用这个函数,会使用一个初始容量值 INIT_CACHE_SIZE
来设定缓存容量;从 INIT_CACHE_SIZE
的定义显示它的值是 4
,也就是说苹果给 cache_t
设定的初始容量是 4
。
重新设置哈希表的长度 _mask = newCapacity-1
,然后将旧内存释放掉,清空缓存;
当通过 find()
方法返回的 bucket->key() == 0
,就说明该位置上是空的,没有缓存过方法,因此可以进行插入操作 bucket->set(key, imp)
,也就是将方法缓存到这个位置上。
注意:传入 cls
得到缓存列表,如果是 instance
对象,返回 class
对象;如果是 class
对象,返回 meta-class
对象;如果是 meta-class
对象,返回 NSObject
的 meta-class
对象;
cache_t 如何查找 sel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); // 通过 cache_hash() 计算出 key 值 k 对应的 index 值 begin,用来记录查询起始索引; mask_t begin = cache_hash(k, m); // begin 赋值给 i,用于切换索引 mask_t i = begin; do { // 用这个 i 从哈希表取值,如果取出来的 bucket_t 的 key = k,则查询成功,返回该 bucket_t。 // 如果 key = 0,说明在索引 i 的位置上还没有缓存过方法,同样需要返回该 bucket_t,用于中止缓存查询。 if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); } static inline mask_t cache_next(mask_t i, mask_t mask) { return (i+1) & mask; } static inline mask_t cache_next(mask_t i, mask_t mask) { return i ? i-1 : mask; }
cache_t
如何查找 sel
,本质上就是根据 key
如何查找 index
的过程; 根据 key
计算出 index
值的这个算法称作哈希算法,尽可能减少不同的 key
得出相同 index
的情况出现,这种情况被称作哈希碰撞,同时还要保证得出的 index
值在合理的范围。index
越大,意味着对应的哈希表的长度越长,这是需要占用实际物理空间的,而内存是有限的。哈希表是一种通过牺牲一定空间,来换取时间效率的设计思想。
SEL objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc
中的表示类型(Swift
中是Selector
类)。selector
是方法选择器,可以理解为区分方法的id
,而这个id
的数据结构是SEL
,即表示一个方法的selector
的指针。
方法的selector
用于表示运行时方法的名字,Objective-C
在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(int
类型的地址),这个标识就是SEL
。
在Objective-C
中,只要方法名相同,那么方法的SEL
就是一样的,每一个方法都对应着一个SEL
,所以在Objective-C
中,同一个类中或者这个类的继承体系中,不能存在2个同名的方法,不同的类可以拥有相同的selector
,不同的类的实例对象执行相同的selector
,会在各自的方法列表中根据selector
去寻找对应的IMP
。
在本质上,SEL
只是一个指向方法的指针(被hash
化得KEY
值),能提高方法的查询速度。
IMP IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
第一个参数是指向 self
的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器( selector
),接下来是方法的实际参数列表。
SEL
就是为了查找方法的最终实现IMP
的,由于每个方法对应唯一的SEL
,因此我们可以通过SEL
方便快速准确地获得它所对应的IMP
。
Method Method
是一种代表类中的某个方法的类型。
而objc_method
在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
注意:
方法名类型为SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。
method_imp
指向了方法的实现,本质上是一个函数指针。
Ivar Ivar
是一种代表类中实例变量的类型。
参考资料https://www.infoq.cn/article/deep-understanding-of-tagged-pointer/ https://draveness.me/isa