前面我们探索了Class
的bits
属性,这篇我们主要探究一下类的第三个属性cache
,了解一下苹果对缓存的处理。我们首先看一下cache
的内存结构。
cache_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 struct cache_t {private : explicit_atomic<uintptr_t > _bucketsAndMaybeMask; union { struct { explicit_atomic<mask_t > _maybeMask; #if __LP64__ uint16_t _flags; #endif uint16_t _occupied; }; explicit_atomic<preopt_cache_t *> _originalPreoptCache; }; void incrementOccupied () ; void setBucketsAndMask (struct bucket_t *newBuckets, mask_t newMask) ; unsigned capacity () const ; struct bucket_t *buckets () const ; Class cls () const ; void reallocate (mask_t oldCapacity, mask_t newCapacity, bool freeOld) ; void insert (SEL sel, IMP imp, id receiver) ; void copyCacheNolock (objc_imp_cache_entry *buffer , int len) ; void destroy () ; void eraseNolock (const char *func) ; static void init () ; static void collectNolock (bool collectALot) ; static size_t bytesForCapacity (uint32_t cap) ; }
对定义有一个大题了解之后,我们像探索bits
一样的方式,用lldb
探索cache
,定义一个类JSPerson
:
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 // JSPerson.h @interface JSPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic) int age; @property (nonatomic, strong) NSString *hobby; - (void)saySomething; - (void)say1; - (void)say2; - (void)say3; - (void)say4; @end // JSPerson.m #import "JSPerson.h" @implementation JSPerson - (void)saySomething{ NSLog(@"%s",__func__); } - (void)say1{ NSLog(@"%s",__func__); } - (void)say2{ NSLog(@"%s",__func__); } - (void)say3{ NSLog(@"%s",__func__); } - (void)say4{ NSLog(@"%s",__func__); } @end int main(int argc, const char * argv[]) { @autoreleasepool { JSPerson *p = [JSPerson alloc]; NSLog(@"%@",p);//断点 } return 0; }
lldb调试查看缓存 在main()
方法初始化JSPerson
对象,使用lldb
调试:
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 80 81 82 83 84 85 86 87 88 89 90 (lldb) p/x JSPerson.class (Class ) $1 = 0x0000000100008660 JSPerson //类首地址 (lldb ) p /x 0x0000000100008670 //便宜16位找到cache (long ) $2 = 0x0000000100008670 (lldb ) p /x (cache_t *)$2 (cache_t *) $3 = 0x0000000100008670 (lldb ) p *$3 (cache_t ) $4 = { _bucketsAndMaybeMask = { std ::__1::atomic<unsigned long > = { Value = 4298437472 } } = { = { _maybeMask = { std ::__1::atomic<unsigned int > = { Value = 0 } } _flags = 32808 _occupied = 0 } _originalPreoptCache = { std ::__1::atomic<preopt_cache_t *> = { Value = 0x0000802800000000 } } } } (lldb) p $4.b uckets() (bucket_t *) $5 = 0x000000010034f360 (lldb) p *$5 (bucket_t ) $6 = { _sel = { std ::__1::atomic<objc_selector *> = (null) { Value = (null) } } _imp = { std ::__1::atomic<unsigned long > = { Value = 0 } } } (lldb) p [p say1] 2021 -06 -27 14 :03 :32.208774 +0800 KCObjcBuild[2660 :112775 ] -[JSPerson say1](lldb) p *$3 (cache_t ) $7 = { _bucketsAndMaybeMask = { std ::__1::atomic<unsigned long > = { Value = 4321236704 } } = { = { _maybeMask = { std ::__1::atomic<unsigned int > = { Value = 7 } } _flags = 32808 _occupied = 1 } _originalPreoptCache = { std ::__1::atomic<preopt_cache_t *> = { Value = 0x0001802800000007 } } } } (lldb) p $7.b uckets() (bucket_t *) $8 = 0x000000010190d6e0 (lldb) p *$8 (bucket_t ) $9 = { _sel = { std ::__1::atomic<objc_selector *> = "" { Value = "" } } _imp = { std ::__1::atomic<unsigned long > = { Value = 48864 } } } (lldb) p $9. sel() (SEL) $11 = "say1" (lldb) p $9. imp(nil,JSPerson.class) (IMP) $12 = 0x0000000100003880 (KCObjcBuild`-[JSPerson say1])
可以看到,我们调用方法之后,cache
里会增加一条缓存。调用buckets()
方法可以查看缓存的内容,buckets()
方法返回值是bucket_t
,我们再看一下bucket_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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 struct bucket_t {private : #if __arm64__ explicit_atomic<uintptr_t > _imp; explicit_atomic<SEL> _sel; #else explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t > _imp; #endif public : static inline size_t offsetOfSel () { return offsetof(bucket_t , _sel); } inline SEL sel () const { return _sel.load(memory_order_relaxed); } #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR #define MAYBE_UNUSED_ISA #else #define MAYBE_UNUSED_ISA __attribute__((unused)) #endif inline IMP rawImp (MAYBE_UNUSED_ISA objc_class *cls) const { uintptr_t imp = _imp.load(memory_order_relaxed); if (!imp) return nil; #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR imp ^= (uintptr_t )cls; #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE #else #error Unknown method cache IMP encoding. #endif return (IMP)imp; } inline IMP imp (UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const { uintptr_t imp = _imp.load(memory_order_relaxed); if (!imp) return nil; #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH SEL sel = _sel.load(memory_order_relaxed); return (IMP) ptrauth_auth_and_resign((const void *)imp, ptrauth_key_process_dependent_code, modifierForSEL(base, sel, cls), ptrauth_key_function_pointer, 0 ); #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR return (IMP)(imp ^ (uintptr_t )cls); #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE return (IMP)imp; #else #error Unknown method cache IMP encoding. #endif } template <Atomicity, IMPEncoding> void set (bucket_t *base, SEL newSel, IMP newImp, Class cls) ; };
bucket_t
通过sel()
方法获取方法的SEL
,通过imp()
方法获取方法的实现IMP
。
脱离源码环境查看缓存 前面我们用lldb
的方法查看了cache
的缓存,如果源码环境不可调试的话,lldb
这种方式就不可用了,我们如果要查看缓存,可以参考一份源码的数据结构,然后打印查看结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits struct js_bucket_t { SEL _sel; IMP _imp; }; struct js_cache_t { struct js_bucket_t *_bukets; // 8 mask_t _maybeMask; // 4 uint16_t _flags; // 2 uint16_t _occupied; // 2 }; struct js_class_data_bits_t { uintptr_t bits; }; // cache class struct js_objc_class { Class isa; Class superclass; struct js_cache_t cache; // formerly cache pointer and vtable struct js_class_data_bits_t bits; };
main()
方法中调用方法,打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main(int argc, const char * argv[]) { @autoreleasepool { JSPerson *p = [JSPerson alloc]; Class pClass = p.class; // objc_clas [p say1]; [p say2]; [pClass sayHappy]; struct js_objc_class *js_class = (__bridge struct js_objc_class *)(pClass); NSLog(@"%hu - %u",js_class->cache._occupied,js_class->cache._maybeMask);//打印occupied和maybeMask for (mask_t i = 0; i<js_class->cache._maybeMask; i++) {//打印缓存方法 struct js_bucket_t bucket = js_class->cache._bukets[i]; NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp); } NSLog(@"Hello, World!"); } return 0; }
先执行say1()
和say2()
方法,打印结果:
1 2 3 4 5 6 7 LGPerson say : -[JSPerson say1] LGPerson say : -[JSPerson say2] LGPerson say : +[JSPerson sayHappy] 2 - 3 say1 - 0xb840f say2 - 0xb810f (null) - 0x0f
可以看到现在缓存是2
个,_maybeMask
=3
,下面我们多调用一个方法:
1 2 3 [p say1]; [p say2]; [p say3];
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12 LGPerson say : -[JSPerson say1] LGPerson say : -[JSPerson say2] LGPerson say : -[JSPerson say3] LGPerson say : +[JSPerson sayHappy] 1 - 7 (null) - 0x0f (null) - 0x0f say3 - 0xb9e8f (null) - 0x0f (null) - 0x0f (null) - 0x0f (null) - 0x0f
我们看到现在occupied
= 1
,_maybeMask
=7
,缓存里只有一个say3
方法,这是为什么,我们就需要看一下源码。本小节提供一个思路可以脱离源码分析cache
的内容,后面我们也可以用类似的思想分析其他问题。
cache
扩容上一小节我们探究过程中发现,调用say3
方法之后,缓存中只有一个say3
方法了,缓存的容量(_maybeMask
)也增加了,其实就是扩容的原因,在什么情况下会触发扩容呢,我们看一下源码,在看catch_t
数据结构中我们发现一个insert
方法,缓存的存储必然会执行这个方法,我们直接看insert
方法:
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 void cache_t::insert (SEL sel, IMP imp, id receiver) { mask_t newOccupied = occupied() + 1 ; unsigned oldCapacity = capacity(), capacity = oldCapacity; if (slowpath(isConstantEmptyCache())) { if (!capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity, false ); } else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { } #if CACHE_ALLOW_FULL_UTILIZATION else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) { } #endif else { capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; } reallocate(oldCapacity, capacity, true ); } bucket_t *b = buckets(); mask_t m = capacity - 1 ; mask_t begin = cache_hash(sel, m); mask_t i = begin ; do { if (fastpath(b[i].sel() == 0 )) { incrementOccupied(); b[i].set <Atomic, Encoded>(b, sel, imp, cls()); return ; } if (b[i].sel() == sel) { return ; } } while (fastpath((i = cache_next(i, m)) != begin )); bad_cache(receiver, (SEL)sel); #endif } static inline mask_t cache_fill_ratio (mask_t capacity) { return capacity * 3 / 4 ; }
可以看出:
初始大小INIT_CACHE_SIZE
=4
,初始maybeMask
=3
触发扩容的条件是(fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))
也就是插入新元素后的容量是否大于等于当前容量的3/4
,如果是就扩容。
扩容的大小是capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
也就是之前容量的2
倍,就是上述例子里say3
执行后的容量=8
。
扩容后不会移动之前缓存的数据,而是直接将新的数据缓存,也就是say3
执行扩容后,缓存里只有一个say3
方法,之前的方法不会移动到新的缓存区。
以上是缓存扩容的流程,为什么扩容的时候之前缓存的内容不会拷贝过来,个人理解是拷贝过来性能较差,如果数据量大影响性能。
总结来看insert
方法的流程如下:
总结
cache
主要是用来存储类的方法缓存的变量,缓存的初始大小为4
。
如果insert
新方法时容量达到缓存大小的3/4
就会触发扩容,扩容后的大小为之前的两倍。
扩容并不会将之前缓存的数据平移到新的空间而是直接释放之前的缓存内存。
缓存的存储方式是哈希表
,哈希的key
是通过cache_hash
方法生成。
如果缓存已存在就直接返回。
如果缓存不存在执行set
方法存储缓存后结束方法。