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<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。


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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容