objec_class: cache_t分析

我们在上个章节类的结构分析中大概描述了一下类的属性,成员变量,实例方法,类方法的存储位置。接下来我们去分析类的结构体cache_t cache做了些什么

附上类的结构体信息

struct objc_class : objc_object {
    // Class ISA;           //8
    Class superclass;       //8
    cache_t cache;          //16        // formerly cache pointer and vtable
    class_data_bits_t bits;  
...省略部分信息...
};

研究cache_t结构体

struct cache_t {
    struct bucket_t *_buckets;  //8
    mask_t _mask;               //4      typedef uint32_t mask_t;  
    mask_t _occupied;           //4

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(SEL sel, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};

结构体里面的属性一一分析

  1. struct bucket_t *_buckets 一个结构体指针,指向结构体bucket_t,里面只有两个私有的属性_imp,_sel.
struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;   //typedef unsigned long           uintptr_t;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
    ...省略部分信息...
};
  1. _mask 一个掩饰的值,到后面的分析可以得到是 hash链表的长度-1。
  2. ** _occupied** 已占用空间。

OC方法在cache_t的cache流程

我们通过bucket_t这个结构体,我们可以很清楚的看到它存的就是 imp和sel,也就是我们的方法。

我们在objc4的源码中 在文件objc_cache.mm的开始位置可以看到有这么一个注释说明

 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
 * cache_fill         (acquires lock)
 * cache_expand       (only called from cache_fill)
 * cache_create       (only called from cache_expand)
 * bcopy               (only called from instrumented cache_expand)
 * flush_caches        (acquires lock)
 * cache_flush        (only called from cache_fill and flush_caches)
 * cache_collect_free (only called from cache_expand and cache_flush)

我们先忽略objc_msgSend这个流程,我们可以直观的看到Cache writes的流程,从cache_fill的故事开始

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

cache_fill的函数里面调用了cache_fill_nolock函数

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // 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.
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
    bucket->set<Atomic>(sel, imp);
}

接下来开始我们的探索之路

  1. 做一个简单的判断,类是否isInitialized,是否可以cache_getImp
    if (!cls->isInitialized()) return;
    if (cache_getImp(cls, sel)) return;
  1. 从类里面拿到cache
 cache_t *cache = getCache(cls);
cache_t *getCache(Class cls) 
{
    assert(cls);
    return &cls->cache;
}
  1. 拿到新的占用空间occupied,通过以后的occupied的值+1
mask_t newOccupied = cache->occupied() + 1;
//occupied() return _occupied;
  1. 拿到cache的容量,是通过_mask值来的,如果初始值为0取0,否在在原有的基础上_mask+1
mask_t capacity = cache->capacity();
mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
    //mask() return _mask;
}
  1. 这是cache重要的一步
    • cache是空的 cache->reallocate
    • 第3步 新的占用空间 <= 容量 的 3/4 就不做处理
    • 如果 新的占用空间 <= 容量 的 3/4 cache->expand()表示占用空间太满了,需要扩容
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        //INIT_CACHE_SIZE 4
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }
  1. 从cache的cache_hash哈希表中通过sel去查找bucket,然后在给bucket设置sel ,imp。
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
    bucket->set<Atomic>(sel, imp);

通过上面的6步流程,我们基本上了解到了cache的write过程了,那么接下来我们对第5步做详细的描述。

  1. cache->isConstantEmptyCache() 顾名思义 判断是一个不变的空的cache,那我们看看它是如何的。
    这里为了更加方便的查看,我会把一些定义的值或者返回的源码直接拿过来放在了这里面。而且注释说明我也会直接写在里面了
bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);

//occupied() return 0;
//buckets()   return _buckets;   _buckets是一个指针还没有初始化 也为0
}
bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true)
{
    cacheUpdateLock.assertLocked();

    // return sizeof(bucket_t) * (cap + 1);
    size_t bytes = cache_t::bytesForCapacity(capacity);
    *
     第一次进来的时候是没有cache的,则capacity = 0,bucket_t结构体的俩个对象都是8字节共16字节 ,则实际上bytes的值就为16 。
    *

    // Use _objc_empty_cache if the buckets is small enough.
    #if DEBUG
    // Use a smaller size to exercise heap-allocated empty caches.
    #   define EMPTY_BYTES ((8+1)*16)
    #else
    #   define EMPTY_BYTES ((1024+1)*16)
    #endif

    //OBJC_EXPORT struct objc_cache _objc_empty_cache

    if (bytes <= EMPTY_BYTES) {
        return (bucket_t *)&_objc_empty_cache;
    }
    *
    走到这个就会直接返回了
    bucket_t *          8                          1000
    _objc_empty_cache   16                        10000
                        &                         00000
    *
    
    //下面的代码与我们的流程没有关系 暂不分析
    // Use shared empty buckets allocated on the heap.
    static bucket_t **emptyBucketsList = nil;
    static mask_t emptyBucketsListCount = 0;
    
    mask_t index = log2u(capacity);

    if (index >= emptyBucketsListCount) {
        if (!allocate) return nil;

        mask_t newListCount = index + 1;
        bucket_t *newBuckets = (bucket_t *)calloc(bytes, 1);
        emptyBucketsList = (bucket_t**)
            realloc(emptyBucketsList, newListCount * sizeof(bucket_t *));
        // Share newBuckets for every un-allocated size smaller than index.
        // The array is therefore always fully populated.
        for (mask_t i = emptyBucketsListCount; i < newListCount; i++) {
            emptyBucketsList[i] = newBuckets;
        }
        emptyBucketsListCount = newListCount;

        if (PrintCaches) {
            _objc_inform("CACHES: new empty buckets at %p (capacity %zu)", 
                         newBuckets, (size_t)capacity);
        }
    }

    return emptyBucketsList[index];
}

这个步骤总结一下:

  • 占用空间occupied是否为空,即_occupied值是否为0;
  • _buckets这个指针是否未初始化,
    bytes 函数的返回值sizeof(bucket_t)&(0 + 1); 然后再和一个EMPTY_BYTES做比较。
  1. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
   //return !isConstantEmptyCache();
    bool freeOld = canBeFreed();
   *
    从上面的流程中可以得到isConstantEmptyCache返回的是0  !0就是true了
   *

    //return _buckets;   一个获取buckets的指针的函数
    bucket_t *oldBuckets = buckets();

    //根据容量 系统开辟一个新的buckets
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
  *
    做了下面几个事 我们可以发现 mask的值为 newCapacity - 1 为3
     _buckets = newBuckets; //_buckets开辟了内存空间
     _mask = newMask;  //_mask赋值为 4 - 1 = 3
     _occupied = 0;
  *
    
    //下面就是清楚旧的buckets 就是把缓存清理干净
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
  • 初始化相对于的值_buckets,_mask,_occupied.
  • 清理缓存
  1. cache->expand(); cache扩容
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
  *
  能进入到扩容的这里面 _mask 是有值的,并且是并且我们知道得到的oldCapacity是_maks + 1,
  申请的一份新的容量是 oldCapacity * 2,我们可以验证一下开辟两倍的空间是最划算的。
  *

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    //这里如同刚开始没有缓存的时候,重新让系统来开辟
    reallocate(oldCapacity, newCapacity);
}

通过把第5步给拎出来,单独探索,我们基本上了解了cache的容量的大小的处理,以及新开辟的内存空间的大小。

接下来我们继续探索第6步。

  1. bucket_t *bucket = cache->find(sel, receiver);
bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);

    bucket_t *b = buckets(); //获取_buckets
    mask_t m = mask(); //获取_mask

    //return (mask_t)(uintptr_t)sel & mask;
    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin); 
    //cache_next(i,m)  return  return (i+1) & mask;
   *
    这里通过一个hash算法,找到我们cache_t中buckets列表里面需要匹配的bucket。
  *
    
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}
  1. if (bucket->sel() == 0) cache->incrementOccupied(); 如果找到的_sel是空的,我们先把已占用的空间加1,一般我们第一次调用的时候,或者是缓存全部清理了之后,这个时候就拿到的_sel会是空的,目的是把我们的当前的_sel再缓存。
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  1. bucket->set<Atomic>(sel, imp); 当前取到的bucket就会赋值自己的属性_imp,_sel。
void bucket_t::set(SEL newSel, IMP newImp)
{
    _imp = (uintptr_t)newImp;
    
    if (_sel != newSel) {
        if (atomicity == Atomic) {
            mega_barrier();
        }
        _sel = newSel;
    }
}

至此为止cache_t方法缓存的流程就分析完毕了。

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

推荐阅读更多精彩内容