Cache_t的结构和原理

在之间的文章里我们分析了isa的指向和结构isa结构分析,分析了bits类的结构分析,在这篇文章里,我们来分析objc_class里面的cache

cache1.png

Cache_t的结构

我们先看下在x86(模拟器)环境下, cache_t的结构

struct cache_t {
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
}
struct bucket_t {
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
}

分析

首先我们先创建一个实例对象person,并且查看其类对象的内存地址

 LYPerson *p = [[LYPerson alloc] init];
[p sayMaster];

(lldb) p/x p.class
(Class)  $0 = 0x00000001000022a0 LYPerson

objc_class的结构我们可以分析得出,cache_t位于其内存偏移 16 字节的位置,根据类对象的首地址和其内存偏移,我们可以得到cache对象

(lldb) p/x 0x00000001000022a0 + 0x10
(long) $1 = 0x00000001000022b0
(lldb) p (cache_t *)0x00000001000022b0
(cache_t *) $2 = 0x00000001000022b0
(lldb) p *$2
(cache_t) $4 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000101a38990 {
      _sel = {
        std::__1::atomic<objc_selector *> = 0x0000000000000000
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 7
  }
  _flags = 32804
  _occupied = 1
}

接下来,让我们来看看Cache_t里面的bucket存放的是什么?通过源码中的buckets()方法,我们可以获取bucket

(lldb) p $4.buckets()
(bucket_t *) $5 = 0x0000000101a38990
(lldb) p *$5
(bucket_t) $7 = {
  _sel = {
    std::__1::atomic<objc_selector *> = 0x0000000000000000
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}

通过lldb,我们验证了,bucket由两部分组成,一个是_sel,一个是_imp,下一步我们要做的就是读取bucket里面的 _sel_imp的值。

(lldb) p $4.buckets()[0].sel()
(SEL) $9 = <no value available>
(lldb) p $4.buckets()[1].sel()
(SEL) $10 = <no value available>
(lldb) p $4.buckets()[2].sel()
(SEL) $11 = <no value available>
(lldb) p $4.buckets()[3].sel()
(SEL) $12 = <no value available>
(lldb) p $4.buckets()[4].sel()
(SEL) $13 = <no value available>
(lldb) p $4.buckets()[5].sel()
(SEL) $14 = "sayMaster"

(lldb) p $4.buckets()[5].imp(pClass)
(IMP) $32 = 0x0000000100000c60 (KCObjc`-[LYPerson sayMaster])

我们可以看出,bucket里面存放的是已经调用过的sel和imp

Cache_t脱离源码环境分析

接下来,我们仿造Cache_t的结构,脱离源码环境进行探索:

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lg_cache_t {
    struct lg_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct lg_class_data_bits_t {
    uintptr_t bits;
};

struct lg_objc_class {
    Class ISA;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

我们来看以下输出

        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];

        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
        NSLog(@"%hu - %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
        for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
            // 打印获取的 bucket
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }

输出结果

2020-09-19 19:26:30.862600+0800 003-cache_t[19867:3905733] LGPerson say : -[LGPerson say1]
2020-09-19 19:26:30.863308+0800 003-cache_t[19867:3905733] LGPerson say : -[LGPerson say2]
2020-09-19 19:26:30.863393+0800 003-cache_t[19867:3905733] LGPerson say : -[LGPerson say3]
2020-09-19 19:26:30.863483+0800 003-cache_t[19867:3905733] LGPerson say : -[LGPerson say4]
2020-09-19 19:26:30.863530+0800 003-cache_t[19867:3905733] 2 - 7
2020-09-19 19:26:30.863667+0800 003-cache_t[19867:3905733] say4 - 0x29a8
2020-09-19 19:26:30.863716+0800 003-cache_t[19867:3905733] (null) - 0x0
2020-09-19 19:26:30.863785+0800 003-cache_t[19867:3905733] say3 - 0x29d8
2020-09-19 19:26:30.863829+0800 003-cache_t[19867:3905733] (null) - 0x0
2020-09-19 19:26:30.863870+0800 003-cache_t[19867:3905733] (null) - 0x0
2020-09-19 19:26:30.863909+0800 003-cache_t[19867:3905733] (null) - 0x0
2020-09-19 19:26:30.863992+0800 003-cache_t[19867:3905733] (null) - 0x0

从上面我可以看出,我们调用了 4个函数方法,但在 buckets里面只存储了2个方法,并且顺序有点问题,并不是按照调用顺序进行存放的。我们带着以下问题在继续探讨:

  • 1,_occupied, _mask 是什么?
  • 2,为什么存放bucket的顺序是乱序?
  • 3,为什么调用的selimp没有全部存起来?

Cache_t的实现原理

当对象调用函数时,_occupied的值会发生变化,我们以此为突破口,在cache_t中发现incrementOccupied方法。然后,查看什么时候调用该函数。
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)方法中调用了该函数。

ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    ASSERT(sel != 0 && cls->isInitialized());

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = occupied() + 1;  // 1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; // 2
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {

        // 4  3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // 扩容两倍 4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 3
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        // 内存 库容完毕
        reallocate(oldCapacity, capacity, true); // 4
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m); // 5
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) { // 6
            incrementOccupied();
            b[i].set<Atomic, Encoded>(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)); // 7

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}
  • 1,获取新的已占用的值。
  • 2,设置容量的初始值 为 4(INIT_CACHE_SIZE == 4)
  • 3,如果 已占用的空间小于整个空间大小的3/4,则对容量进行2倍扩容。
  • 4,扩容只会重新申请空间,对之前的数据并没有进行复制。
  • 5,将selmask(capacity - 1)进行与运算,得到小于capacity索引值i
  • 6,如果buckets[i]处,没有存放bucket,则将其放到i处。
  • 7,如果buckets[i]处,已经存放了bucket,那么说明发生了哈希冲突,则使用开放地址法,从尾部开始查找空桶,将其放入空桶中。

通过以上分析我们可以得出问题的答案了。

  • 1,_occupied:表示当前 buckets()里面的bucket数量。_mask等于buckets容量 - 1
  • 2,bucket存放的位置是由sel哈希值&capacity - 1决定的,与方法的调用顺序无关,所以是无序的。
  • 3,当buckets_occupied> capacity * 3/4时,buckets会进行扩容,会对buckets进行重新开辟内存,导致之前存放的bucket会丢失。

总结

在这篇文章里,我们先分析了 cache_t的结构,紧接着,我们脱离源码来分析cache_t,最后一部分,我们结合源码分析了cache_t实现的原理。

`

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