当前位置: 首页>编程语言>正文

OC底层探索之cache详解

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

OC底层探索之cache详解,第1张
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到底存了什么东西。

OC底层探索之cache详解,第2张
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结构体:

OC底层探索之cache详解,第3张
打印cache内容

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

OC底层探索之cache详解,第4张
定位buckets

打印buckets,打印bucket_t结构体内容,我们也没发现有啥有效信息,很懵逼。
OC底层探索之cache详解,第5张
打印buckets

那我们在看看bucket_t结构体,我们发现有_sel_imp这2个参数和sel()方法。尝试着打印_sel看看。
OC底层探索之cache详解,第6张
bucket_t结构体

OC底层探索之cache详解,第7张
bucket_t结构体

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

OC底层探索之cache详解,第8张
打印方法

cache扩容分析

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

OC底层探索之cache详解,第9张
发现insert方法

点击查看insert方法。我们发现insert方法内部有个set方法,点击set方法内部我们发现一个store方法,store就是存储的意思,终于找到具体方法存储方法了。
OC底层探索之cache详解,第10张
insert方法具体实现
OC底层探索之cache详解,第11张
insert方法的set方法
OC底层探索之cache详解,第12张
set方法内部store函数

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


OC底层探索之cache详解,第13张
定位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<Atomic, Encoded>(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。


OC底层探索之cache详解,第14张
occupied()内部实现

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

    unsigned oldCapacity = capacity(), capacity = oldCapacity;
OC底层探索之cache详解,第15张
capacity()函数

OC底层探索之cache详解,第16张
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);
    }
OC底层探索之cache详解,第17张
INIT_CACHE_SIZE
OC底层探索之cache详解,第18张
CACHE_END_MARKER
OC底层探索之cache详解,第19张
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,满足条件,是不需要扩容了。

OC底层探索之cache详解,第20张
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.
    }
OC底层探索之cache详解,第21张
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);
    }
OC底层探索之cache详解,第22张
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,所以说没有进行扩容。

OC底层探索之cache详解,第23张
2个方法扩容验证

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

OC底层探索之cache详解,第23张
5个方法扩容验证

当调用3个方法的时候,可以打印method3,没有打印method1method2,所以进行扩容了,现在桶子大小是8。
(lldb) p objc_getClass("LGPerson")
(Class) method8 = 0x0000000100008288
(lldb) x/4gx 0x0000000100008288
0x100008288: 0x0000000100008260 0x0000000100800140
0x100008298: 0x0000000100b5f510 0x0001801000000007
(lldb) p (cache_t *)0x100008298
(cache_t *)  = 0x0000000100008298
(lldb) p ->buckets()
(bucket_t *)  = 0x0000000100b5f510
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +1
(bucket_t *)  = 0x0000000100b5f520
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +2
(bucket_t *)  = 0x0000000100b5f530
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +3
(bucket_t *)  = 0x0000000100b5f540
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +4
(bucket_t *)  = 0x0000000100b5f550
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +5
(bucket_t *)  = 0x0000000100b5f560
(lldb) p ->sel()
(SEL)  = (null)
(lldb) p +6
(bucket_t *)  = 0x0000000100b5f570
(lldb) p ->sel()
(SEL)  = "method3"
(lldb) p +7
(bucket_t *)  = 0x0000000100b5f580
(lldb) p ->sel()
(SEL)  = ""
(lldb) p +8
(bucket_t *)  = 0x0000000100b5f590
(lldb) p ->sel()
(SEL)  = (null)

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

OC底层探索之cache详解,第25张
调用8个方法

(lldb) p objc_getClass("LGPerson") (Class) = 0x00000001000082b0 (lldb) x/4gx 0x00000001000082b0 0x1000082b0: 0x0000000100008288 0x0000000100800140 0x1000082c0: 0x0000000100c40d50 0x000180100000000f (lldb) p (cache_t *)0x1000082c0 (cache_t *) = 0x00000001000082c0 (lldb) p ->buckets() (bucket_t *) = 0x0000000100c40d50 (lldb) p ->sel() (SEL) = (null) (lldb) p +1 (bucket_t *) = 0x0000000100c40d60 (lldb) p ->sel() (SEL) = (null) (lldb) p +2 (bucket_t *) = 0x0000000100c40d70 (lldb) p ->sel() (SEL) = (null) (lldb) p +3 (bucket_t *) = 0x0000000100c40d80 (lldb) p ->sel() (SEL) = (null) (lldb) p +4 (bucket_t *) = 0x0000000100c40d90 (lldb) p ->sel() (SEL) = (null) (lldb) p +5 (bucket_t *) = 0x0000000100c40da0 (lldb) p ->sel() (SEL) = (null) (lldb) p +6 (bucket_t *) = 0x0000000100c40db0 (lldb) p ->sel() (SEL) = (null) (lldb) p +7 (bucket_t *) = 0x0000000100c40dc0 (lldb) p ->sel() (SEL) = (null) (lldb) p +8 (bucket_t *) = 0x0000000100c40dd0 (lldb) p ->sel() (SEL) = (null) (lldb) p +9 (bucket_t *) = 0x0000000100c40de0 (lldb) p ->sel() (SEL) = (null) (lldb) p +10 (bucket_t *) = 0x0000000100c40df0 (lldb) p ->sel() (SEL) = (null) (lldb) p +11 (bucket_t *) = 0x0000000100c40e00 (lldb) p ->sel() (SEL) = (null) (lldb) p +12 (bucket_t *) = 0x0000000100c40e10 (lldb) p ->sel() (SEL) = (null) (lldb) p +13 (bucket_t *) = 0x0000000100c40e20 (lldb) p ->sel() (SEL) = (null) (lldb) p +14 (bucket_t *) = 0x0000000100c40e30 (lldb) p ->sel() (SEL) = "method8" (lldb) p +15 (bucket_t *) = 0x0000000100c40e40 (lldb) p ->sel() (SEL) = "" (lldb) p +16 (bucket_t *) = 0x0000000100c40e50 (lldb) p ->sel() (SEL) = (null) (lldb) p +17 (bucket_t *) = 0x0000000100c40e60 (lldb) p ->sel() (SEL) = "" (lldb) p +18 (bucket_t *) = 0x0000000100c40e70 (lldb) p ->sel() (SEL) = (null)


https://www.xamrdz.com/lan/5gm2016161.html

相关文章: