The memory structure of cache_t
In class structure and class_datA_bits_t, we analyze class structure, including ISA, superclass, cache, bits. Today we’ll look at the structure of cache_t.
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
Copy the code
If you look at the source code, the underlying structure of cache_t looks like this, but you don’t see anything special here, so if you look down, you see a number of methods associated with bucket_t, look at the structure, and, hey, sel IMP. The class cache is used to cache methods. Called methods are cached in cache_t and can be fetched directly from the cache the next time the method is called.
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;
};
}
Copy the code
struct bucket_t { private: // IMP-first is better for arm64e ptrauth and no worse for arm64. // SEL-first is better for armv7* and i386 and x86_64. #if __arm64__ explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; #else explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t> _imp; #endif }Copy the code
LLDB fetches methods in cache_t
Define a class, call object method, through the source LLDB, outcache_t
The method of caching in.
(lldb) p/x SwwPerson.class (Class) $0 = 0x0000000100008768 SwwPerson (lldb) p (cache_t *)0x0000000100008778 (cache_t *) $1 = 0x0000000100008778 (lldb) p *$1 (cache_t) $2 = { _bucketsAndMaybeMask = { std::__1::atomic<unsigned long> = { Value = 4302622544 } } = { = { _maybeMask = { std::__1::atomic<unsigned int> = { Value = 3 } } _flags = 32812 _occupied = 1 } _originalPreoptCache = { std::__1::atomic<preopt_cache_t *> = { Value = 0x0001802c00000003 } } } } (lldb) p $1->buckets() (bucket_t *) $3 = 0x000000010074cf50 (lldb) p *$3 (bucket_t) $4 = { _sel = { std::__1::atomic<objc_selector *> = (null) { Value = nil } } _imp = { std::__1::atomic<unsigned long> = { Value = 0 } } } (lldb) p $1->buckets()[1] (bucket_t) $5 = { _sel = { std::__1::atomic<objc_selector *> = "" { Value = "" } } _imp = { std::__1::atomic<unsigned long> = { Value = 48616 } } } (lldb) p $5.sel (SEL) $6 = "sayHi" Fix-it applied, fixed expression was: $5.sel() (lldb) p $1->buckets()[2]Copy the code
Cache insertion process
If the cache is occupied for the first time: “occupied + 1”; if the cache is initialized for the first time: “occupied + 1”; When you insert the method again, occupied = 2, occupied + 1 <= 3/4, so do nothing. Occupied = 3; capacity*2 = 8; the cached methods will be cleared.
Why 3/4 is chosen as the size of expansion? There is a saying that the load factor, 0.75 is the highest space utilization. And when using hash function to calculate subscripts, it can effectively avoid conflicts and improve efficiency.
if (slowpath(isConstantEmptyCache())) { // Cache is read-only. Replace it. if (! capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity, /* freeOld */false); Else if (fastPath (newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {// Cache is less than 3/4 or 7/8 full. Use it as-is. } CACHE_END_MARKER = 1 else { capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; } reallocate(oldCapacity, capacity, true); }Copy the code