OC底层原理06-cache_t探究

iOS--OC底层原理文章汇总

前言

本文主要探索cache_t * cache结构内容,分析它在类的结构中扮演了什么样的角色。
通过前面OC底层原理04的介绍,我们对类的结构有了一个清晰的了解,总结如下

Class结构

OC底层原理03— isa探究中,通过objc_object源码 对isa进行了探究,OC底层原理04对bits做了探索。现在来探究cachecache字面意思--缓存很好理解,那它缓存了什么内容呢?已经它缓存的结构又是怎么样呢?
缓存: 缓存了增删改查操作 ,目的为了操作安全。每调用一次方法,就缓存下来。

从源码出发

我们来看看cache_t源码

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets; 
// explicit_atomic:显示原子性,为了在缓存增删改查操作的操作安全,提高线程的安全性。
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets; //arm64与mac环境不一样,将二者结合在一起使用,目的是优化,提高效率。
    mask_t _mask_unused;
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; // 类似isa的面具
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags; // 
#endif
    uint16_t _occupied; // 

结构体CACHE_MASK_STORAGE的三种情况:
CACHE_MASK_STORAGE_OUTLINED: 模拟器 \ macOS 环境
CACHE_MASK_STORAGE_HIGH_16: 真机环境
CACHE_MASK_STORAGE_LOW_4: 不是64位的真机环境
其他属性:
bucke_t: 装着imp , sel
mask:类似isa中的mask(面具)
_flags: 位置标记
_occupied: 占位,缓存占用量

通过图我们再来宏观上感受下cache_t的结构

  • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    cache_t的结构
  • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    maskAndBucket时cache_t结构

LLDB调试之

objc-781基础上定义一个类,编写一些方法:

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayHello;

- (void)sayCode;

- (void)sayMaster;

- (void)sayNB;

+ (void)sayHappy;

@end
// LGPerson.m
#import "LGPerson.h"

@implementation LGPerson
- (void)sayHello{
    NSLog(@"LGPerson say : %s",__func__);
}

- (void)sayCode{
    NSLog(@"LGPerson say : %s",__func__);
}

- (void)sayMaster{
    NSLog(@"LGPerson say : %s",__func__);
}

- (void)sayNB{
    NSLog(@"LGPerson say : %s",__func__);
}

+ (void)sayHappy{
    NSLog(@"LGPerson say : %s",__func__);
}
@end

在main.m中调用它,在[p sayHello];打下断点,在调试窗口进行调试

 LGPerson *p  = [LGPerson alloc];
 Class pClass = [LGPerson class];
 [p sayHello];
 [p sayCode];
 [p sayMaster];

按照之前宏观下分析的结构,调试结果如下:

(lldb) p/x pClass // 打印类的指针地址
(Class) $0 = 0x0000000100002298 LGPerson
(lldb) p (cache_t *)0x00000001000022a8 // 采用获取bits类似的方式获取cacche,这里指针移动16位,即加上0x10.
(cache_t *) $1 = 0x00000001000022a8
(lldb) p *$1 // 打印出cache_t的结构
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010032e430 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 0
  }
  _flags = 32804
  _occupied = 0
}
2020-09-20 00:03:34.346793+0800 KCObjc[8619:340927] LGPerson say : -[LGPerson sayHello]
(lldb) p *$1
(cache_t) $3 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010072c870 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11928
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}

这里做了一个操作,当第一次p *$1时断点停留在 [p sayHello];,然后断点走到下一步,代码执行了 -[LGPerson sayHello]操作,再次执行p *$1_sel、_imp、_flags 、_occupied的值发生了变化。初步验证每调用一次方法,就将其缓存。
我们继续调试

(lldb) p $3.buckets() // 打印buckets数组
(bucket_t *) $4 = 0x000000010072c870
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11928
  }
}
(lldb) p $5.sel() // sel()调用方式是查找cache_t下的定义方法确定的
(SEL) $6 = "sayHello"
(lldb) p $5.imp(pClass)  // 同样查看imp是否有相关实现方法:imp(Class cls)
(IMP) $7 = 0x0000000100000c00 (KCObjc`-[LGPerson sayHello]) // 打印出带具体方法实现的函数指针 imp

我们再通过MachOView软件查找可执行文件中的方法以验证正确性。软件具体使用可参考这里

MachOView验证

结果显示一致。

脱离源码环境分析

通过源码我们能初探到了 cache_t中的结构,现在我们再脱离源码环境基础上再来分析下。
新建一个mac工程,创建一个Book类,属性及方法具体如下:

@interface Book : NSObject
@property (nonatomic,copy) NSString * bookName;
@property (nonatomic,copy) NSString * version;

- (void)read1;
- (void)read2;
- (void)read3;
- (void)read4;
@end
// Book.m
#import "Book.h"
@implementation Book
- (void)read1{
    NSLog(@"read book : %s",__func__);
}
- (void)read2{
    NSLog(@"read book : %s",__func__);
}
- (void)read3{
    NSLog(@"read book : %s",__func__);
}
- (void)read4{
    NSLog(@"read book : %s",__func__);
}
@end

在main.m中从源码工程中组建一份与cache_t结构相似的代码,去除一些非本研究内容:


#import <Foundation/Foundation.h>
#import "Book.h"
#import <objc/runtime.h>
typedef uint32_t mask_t;

struct my_buckt_t { // bucket中存储的就是sel,imp,所以保留两个即可
    SEL _sel;
    IMP _imp;
};
struct my_cache_t { // 只保留cache_t中重要的研究对象
    struct my_buckt_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};
struct my_class_data_bits_t{  // 简化bits
    uintptr_t bits;
};
struct my_objc_class { // 取消继承自objc_object
    Class ISA; // 由于未继承了,所以得打开isa注释
    Class superclass;
    struct my_cache_t cache;             // formerly cache pointer and vtable
    struct my_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book * book = [Book alloc];
        Class bClass = [Book class];
//        book.bookName = @"Object-C";
//        book.version = @"1.0.0";
        
        [book read1];
//        [book read2];
//        [book read3];
        struct my_objc_class * my_bClass = (__bridge struct my_objc_class *)(bClass);
        NSLog(@"occupied:%hu - mask:%u",my_bClass->cache._occupied, my_bClass->cache._mask);
        for (mask_t i = 0; i < my_bClass->cache._mask; i ++) {
            // 打印获取的bucket
            struct my_buckt_t  bucket = my_bClass->cache._buckets[I];
            NSLog(@"sel:%@ - imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
    }
    return 0;
}

执行之后会得到下面结果


其中_occupied = 1, mask = 3,在打印出的buckets数组中,只有第一个有值。当我们打开read2,read3的注释,结果就会是这样:

随着方法调用的增加,mask的值发生了变化。而sel、imp并没有随着调用方法增加,而增加值,依然是有很多的控制。那么occupied,mask是什么?底层是怎么做的?接下来我们继续本文重点内容,cache底层原理。

cache底层原理

我们切换至781源码工程,再找到cache_t结构定义文件,搜索occupied,我们找到这样的代码,找到了增加occupied的函数:


搜索调用改方法的地方找到了

// 调用之处,自身加1
void cache_t::incrementOccupied() 
{
    _occupied++;
}
//---
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());
    // 当缓存少于3/4时,按原样使用
    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 <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 4
        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 because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            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));
    cache_t::bad_cache(receiver, (SEL)sel, cls);
}
1.计算缓存占用量

mask_t newOccupied = occupied() + 1;这里是新建一个newOccupied,这一步是为了计算缓存占用量。这里有个需要注意的点,occupied的值在[p init]时,occupied = 1;当给属性赋值时会调用set方法,也会是得occupied的值增加。在脱离源码分析的工程中,我们未调用init、属性,这就解释了我们打印的occupied的为1。

2.开辟空间
  • 1)默认开辟4个单位的缓存空间
  • 2)缓存占用量少于 3/4 时,不做操作
  • 3)其他情况,在原缓存空间基础上扩容2倍。
if (slowpath(isConstantEmptyCache())) { // 如果未空,执行初始化操作
        // Cache is read-only. Replace it. 
        if (!capacity) capacity = INIT_CACHE_SIZE; // INIT_CACHE_SIZE = 1 << 2 = 0100 = 4
        reallocate(oldCapacity, capacity, /* freeOld */false); // 初始化 开辟空间
 } else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
 }else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍  2 * 4 
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);  // 内存库容完毕
    }

reallocate

3.缓存bucket - sel & mask
    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 because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            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));
    cache_t::bad_cache(receiver, (SEL)sel, cls);

mask_t m = capacity - 1;就是之前脱离源码环境分析的sel = 3,mask = 7,未扩容和扩容的两个情况。
mask_t begin = cache_hash(sel, m); 这个是用到哈希算法。如果在之前基础上进行了扩容,那么利用哈希算法就得找到之前存储的位置进行重排存储。计算得到sel下标,
1).如果第一次进入存储,那么sel的下标是从0开始存储,此时occupied从0开始加1;
2).当第二次进入的存储过程,sel下标如果和即将插入存储的sel相同,则进行原样存储;
3).当再次新存储的sel在遍历bucket存储空间,已经存储,那么就要cache_next,算法重排,再次计算一个新的下标进行存储。
所以在bucket扩容后,会重新生成,所以会有看起来原来位置的sel为空,其实只是梳理了空间,换了个存储位置,而这个位置是乱序查找插入的,非遍历形式的好处是存储和取出的效率更高。
cache_hash:对存储进行哈希

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask; // mask = capacity -1 ,sel & mask
}

cache_next哈希重排

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;  // 1 & 7 = 1, 2 & 7 = 2, 1则就可以作为返回值以供查找下标
}

总结

通过一顿操作探究,我们得到了以下的信息:
1.在cache中,occupied是一个缓存占用量,用来判断是否需要进行扩容,大于容量的3/4时就必须要扩容;
2.mask字面意思数面具,是哈希存储过程中的下标。它等于 capacity - 1;,如果扩容之后是8 ,那么在利用哈希存储的时候,最高就到7,不然就要越界了;
3.在方法调用(包括init)、属性设置过程中,都会引起occupied的增加;
4.扩容后的cache,在存储bucket时,是利用哈希算法存储的,哈希算法存储会比遍历效率更高。

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