OC底层探索之cache详解

我们在OC底层探索之对象原理(下)探索了isa指针指向,在OC底层探索之类的探索(上)OC底层探索之类的探索(下))探索了rorwcache顾名思义是缓存,它到底缓存了什么怎么缓存的你?今天我们来探索cache

objc_class结构体

初探cache_t结构体

我们先看下cache_t结构体结构,首先是一个_bucketsAndMaybeMask它是uintptr_t类型的占8字节内存;然后是一个联合体,我们知道联合是互质的,它是按照联合体内部最大成员变量来计算内存,_originalPreoptCache是占8字节内存的,_maybeMask是占4字节内存,_flags是占2字节内存的,_occupied是占2字节内存的,所以联合体内部的结构体占8字节内存,联合体占8字节内存,整个cache_t结构体是占16字节内存。所以根据内存平移规则我们获取bits数据时需要平移isa(8字节)+superclass(8字节)+cache(16字节)=32个字节的内存。
当然我们看cache_t结构体内部数据看不出来什么。使用lldb打印来看cache_t到底存了什么东西。

cache_t结构体

我们使用objc-838进行探索,新建一个LGPersion,里面包含method1method2method3method4等实例方法,method5method6等类方法。

@interface LGPerson : NSObject
- (void)method1;
- (void)method2;
- (void)method3;
- (void)method4;

+ (void)method5;
+ (void)method6;

@end

打印cache内容,我们也没发现有啥有效信息,很懵逼。那我们在看看cache_t结构体:

打印cache内容

我们发现cache结构体里面有个buckets函数返回一个bucket_t的结构体。

定位buckets

打印buckets,打印bucket_t结构体内容,我们也没发现有啥有效信息,很懵逼。
打印buckets

那我们在看看bucket_t结构体,我们发现有_sel_imp这2个参数和sel()方法。尝试着打印_sel看看。
bucket_t结构体

bucket_t结构体

打印出来我们发现只有classrespondsToSelector:这2个方法,很是懵逼,没有我们的method1method2,这是为什么呢?难道我们的method1method2没有被缓存吗?带着这个疑问我们再回来看看cache_t这个结构体。

打印方法

cache扩容分析

我们初探cache_t结构体时没有发现我们的的method1method2方法,那cache是到底是怎么缓存方法的呢?我们在cache_t结构体内部发现了insert方法(顾名思义插入方法)里面回传3个参数sel(方法名)、imp(方法实现)、receiver(接收者)。

发现insert方法

点击查看insert方法。我们发现insert方法内部有个set方法,点击set方法内部我们发现一个store方法,store就是存储的意思,终于找到具体方法存储方法了。
insert方法具体实现

insert方法的set方法
set方法内部store函数

那么就开始分析cache到底是怎么个操作流程。我们直接定位到insert方法有效代码段。


定位insert方法有效代码段

insert方法有效代码段:

    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    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.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#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;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);

首先我们先看下occupied()是这啥意思,可以看到occupied()内部实现,它直接返回一个_occupied,而_occupied是cache_t里面结构体的一个成员变量,初始化的时候它是0。所以newOccupied=occupied()+1 = 0+1 =1。


occupied()内部实现

我们可以看到capacity = oldCapacity而oldCapacity = capacity(),看看capacity()内部实现,它是一个三目运算,点击进去mask()函数,我们可以看到其实它就是加载cache_t内部_maybeMask的数量,当然当第一次的是0,之后都是数量+1,其实也就是buckets的长度-1,那newOccupied的长度其实就是buckets的长度。

    unsigned oldCapacity = capacity(), capacity = oldCapacity;

capacity()函数

mask()函数

当我们第一次进来的时候cache是空的,capacity = INIT_CACHE_SIZE,我们可以看到INIT_CACHE_SIZE在里面有个CACHE_END_MARKERCACHE_END_MARKER定义在x86_64下是1在arm64是0,所以说INIT_CACHE_SIZEx86_64下是1<<2 = 4,在arm64下是1<<1 =2。所以capacityx86_64下是4,在arm64下是2。然后我们看看reallocate函数具体实现,setBucketsAndMask它是设置cache_t结构体内部成员变量_bucketsAndMaybeMaskfreeOld如果是true它会释放oldBuckets,是false啥也不做。第一次来的时候freeOldfalse所以不会被释放。

if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
INIT_CACHE_SIZE
CACHE_END_MARKER
reallocate函数
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {

我们之前分析newOccupied是1,CACHE_END_MARKERx86_64下是1在arm64是0。capacitybucket的长度,cache_fill_ratio函数则是在x86_64下是bucket长度的4分之3,在arm64是bucket长度的8分之7。newOccupied + CACHE_END_MARKER实际就是缓存的大小。
总结一下:缓存大小在arm64下小于或等于bucket(桶子)的8分之7或者在x86_64下小于或等于bucket(桶子)的4分之3,啥也不做。需要注意的是CACHE_END_MARKERx86_64下是1在arm64是0。当桶子的个数是4个的时候,第3个方法进来的时候,在x86_64newOccupied + CACHE_END_MARKER是4,不满足条件,就需要扩容了,在x86_64newOccupied + CACHE_END_MARKER是3,满足条件,是不需要扩容了。

cache_fill_ratio

FULL_UTILIZATION_CACHE_SIZE的长度等于1<<3 = 8,所以在arm64架构下当桶子的长度小于8的时候啥也不做。

else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }

FULL_UTILIZATION_CACHE_SIZE

最后一个判断,当桶子的大小为0的时候,会给一个初始值INIT_CACHE_SIZE(我们上文提到INIT_CACHE_SIZEx86_64下是4,在arm64下是2),如果桶子大小不为0会进行2倍扩容。当桶子大小大于MAX_CACHE_SIZE(1<<16为2的16次方),桶子大小为MAX_CACHE_SIZE。我们上文分析reallocate函数第三个参数freeOldtrue的时候,老桶子会被释放。所以说我们在初探cache_t结构体的method1method2没被发现的原因,可能是cache扩容了,method1method2被释放了。

else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }
MAX_CACHE_SIZE

总结:

cache扩容规则

  • x86_64
    1.当缓存的大小等于的4分之3时候,会进行2倍扩容。
  • arm64
    1.当缓存的大小大于桶子长度8分之7的时候,进行2倍扩容
    2.当桶子的长度小于8的时候,不会扩容。

cache扩容验证

我们采用Intel芯片的模拟器,是x86_64环境的。所以说初始化桶子的大小是4。
我们通过之前分析,当前需要缓存数量(缓存的大小+1)到3个、8个、14个等等的时候,是需要扩容,好了,验证一下。
p objc_getClass("LGPerson"):使用这个是为了防止之前p p.class的时候会生成classrespondsToSelector:这2个方法。
当调用2个方法的时候,可以打印method1method2,所以说没有进行扩容。

2个方法扩容验证

当调用2个方法的时候,可以打印method1method2,所以说没有进行扩容,现在桶子大小是4。

5个方法扩容验证

当调用3个方法的时候,可以打印method3,没有打印method1method2,所以进行扩容了,现在桶子大小是8。

(lldb) p objc_getClass("LGPerson")
(Class) $0 = 0x0000000100008288
(lldb) x/4gx 0x0000000100008288
0x100008288: 0x0000000100008260 0x0000000100800140
0x100008298: 0x0000000100b5f510 0x0001801000000007
(lldb) p (cache_t *)0x100008298
(cache_t *) $1 = 0x0000000100008298
(lldb) p $1->buckets()
(bucket_t *) $2 = 0x0000000100b5f510
(lldb) p $2->sel()
(SEL) $3 = (null)
(lldb) p $2+1
(bucket_t *) $4 = 0x0000000100b5f520
(lldb) p $4->sel()
(SEL) $5 = (null)
(lldb) p $2+2
(bucket_t *) $6 = 0x0000000100b5f530
(lldb) p $6->sel()
(SEL) $7 = (null)
(lldb) p $2+3
(bucket_t *) $8 = 0x0000000100b5f540
(lldb) p $8->sel()
(SEL) $9 = (null)
(lldb) p $2+4
(bucket_t *) $10 = 0x0000000100b5f550
(lldb) p $10->sel()
(SEL) $11 = (null)
(lldb) p $2+5
(bucket_t *) $12 = 0x0000000100b5f560
(lldb) p $12->sel()
(SEL) $13 = (null)
(lldb) p $2+6
(bucket_t *) $14 = 0x0000000100b5f570
(lldb) p $14->sel()
(SEL) $15 = "method3"
(lldb) p $2+7
(bucket_t *) $16 = 0x0000000100b5f580
(lldb) p $16->sel()
(SEL) $17 = ""
(lldb) p $2+8
(bucket_t *) $18 = 0x0000000100b5f590
(lldb) p $18->sel()
(SEL) $19 = (null)

当调用8个方法的时候,可以打印method8,没有打印method1method2method3method4method5method6method7,所以进行扩容了,原来桶子大小是8,原桶子的数据被释放,扩容后的大小是16。

调用8个方法

lldb调试结果:


(lldb) p objc_getClass("LGPerson")
(Class) $0 = 0x00000001000082b0
(lldb) x/4gx 0x00000001000082b0
0x1000082b0: 0x0000000100008288 0x0000000100800140
0x1000082c0: 0x0000000100c40d50 0x000180100000000f
(lldb) p (cache_t *)0x1000082c0
(cache_t *) $1 = 0x00000001000082c0
(lldb) p $1->buckets()
(bucket_t *) $2 = 0x0000000100c40d50
(lldb) p $2->sel()
(SEL) $3 = (null)
(lldb) p $2+1
(bucket_t *) $4 = 0x0000000100c40d60
(lldb) p $4->sel()
(SEL) $5 = (null)
(lldb) p $2+2
(bucket_t *) $6 = 0x0000000100c40d70
(lldb) p $6->sel()
(SEL) $7 = (null)
(lldb) p $2+3
(bucket_t *) $8 = 0x0000000100c40d80
(lldb) p $8->sel()
(SEL) $9 = (null)
(lldb) p $2+4
(bucket_t *) $10 = 0x0000000100c40d90
(lldb) p $10->sel()
(SEL) $11 = (null)
(lldb) p $2+5
(bucket_t *) $12 = 0x0000000100c40da0
(lldb) p $12->sel()
(SEL) $13 = (null)
(lldb) p $2+6
(bucket_t *) $14 = 0x0000000100c40db0
(lldb) p $14->sel()
(SEL) $15 = (null)
(lldb) p $2+7
(bucket_t *) $16 = 0x0000000100c40dc0
(lldb) p $16->sel()
(SEL) $17 = (null)
(lldb) p $2+8
(bucket_t *) $18 = 0x0000000100c40dd0
(lldb) p $18->sel()
(SEL) $19 = (null)
(lldb) p $2+9
(bucket_t *) $20 = 0x0000000100c40de0
(lldb) p $20->sel()
(SEL) $21 = (null)
(lldb) p $2+10
(bucket_t *) $22 = 0x0000000100c40df0
(lldb) p $22->sel()
(SEL) $23 = (null)
(lldb) p $2+11
(bucket_t *) $24 = 0x0000000100c40e00
(lldb) p $24->sel()
(SEL) $25 = (null)
(lldb) p $2+12
(bucket_t *) $26 = 0x0000000100c40e10
(lldb) p $26->sel()
(SEL) $27 = (null)
(lldb) p $2+13
(bucket_t *) $28 = 0x0000000100c40e20
(lldb) p $28->sel()
(SEL) $29 = (null)
(lldb) p $2+14
(bucket_t *) $30 = 0x0000000100c40e30
(lldb) p $30->sel()
(SEL) $31 = "method8"
(lldb) p $2+15
(bucket_t *) $32 = 0x0000000100c40e40
(lldb) p $32->sel()
(SEL) $33 = ""
(lldb) p $2+16
(bucket_t *) $34 = 0x0000000100c40e50
(lldb) p $34->sel()
(SEL) $35 = (null)
(lldb) p $2+17
(bucket_t *) $36 = 0x0000000100c40e60
(lldb) p $36->sel()
(SEL) $37 = ""
(lldb) p $2+18
(bucket_t *) $38 = 0x0000000100c40e70
(lldb) p $38->sel()
(SEL) $39 = (null)

你可能感兴趣的:(OC底层探索之cache详解)