美文网首页
iOS底层系列11 -- 类的cache成员分析

iOS底层系列11 -- 类的cache成员分析

作者: YanZi_33 | 来源:发表于2021-02-20 17:56 被阅读0次
  • 在objc_class即类的结构体中存在一个cache_t结构体类型成员cache,其主要做缓存操作的,下面我们来探索它的实现原理;
  • 首先给出cache_t结构体的源码:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

    方法省略......
};
  • 此源码去除了方法和static变量;
  • 从源码可以看出,cache是分为三种架构进行处理的;
    • CACHE_MASK_STORAGE_OUTLINED 表示运行的环境 模拟器 或者 macOS
    • CACHE_MASK_STORAGE_HIGH_16 表示运行环境是 64位的真机
    • CACHE_MASK_STORAGE_LOW_4 表示运行环境是 非64位 的真机
  • 在真机的运行环境下,Cache_t的内部成员有如下:
    • _maskAndBuckets:mask和buckets是写在一起,目的是为了优化,可以通过各自的掩码来获取相应的数据,即explicit_atomic<uintptr_t> _maskAndBuckets,占8个字节64位,高16位表示mask,低48表示buckets,其中mask表示掩码,其值等于 (散列表的容量 -1),buckets表示存储bucket_t结构体的散列表,也就是所谓的哈希表,bucket_t也是一个结构体,其是用来存储方法的;
    • _mask_unused:属于mask_t类型,本质是unsigned int类型 占4个字节;
    • _flags:属于uint16_t类型,本质是unsigned short类型 占2个字节;
    • _occupied:属于uint16_t类型,本质是unsigned short类型 占2个字节,用来记录哈希表Buckets存储方法的个数;
  • bucket_t的结构体定义如下:
struct bucket_t {
  private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
}
  • 从bucket_t的定义可以看出,同样分为两个版本,真机 和 非真机,不同的区别在于sel 和 imp的顺序不一致;

  • 总结:

    • 通过上面两个结构体源码可知,bucket_t结构体中存储的是方法method的sel - imp(方法名 - 方法体)的键值对;
    • buckets是存储bucket_t结构体的散列表(哈希表);

根据测试代码,通过LLDB调试来分析cache_t的工作原理

  • 测试代码如下:
@interface YYPerson : NSObject

@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)NSInteger weight;

- (void)walk;
- (void)eat;
- (void)say;
- (void)speak;
- (void)write;

+ (void)sing;
+ (void)work;

@end
@implementation YYPerson

- (void)walk{
    NSLog(@"%s",__func__);
}

- (void)eat{
    NSLog(@"%s",__func__);
}

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

- (void)speak{
    NSLog(@"%s",__func__);
}

- (void)write{
    NSLog(@"%s",__func__);
}

+ (void)sing{
    NSLog(@"%s",__func__);
}

+ (void)work{
    NSLog(@"%s",__func__);
}

@end
Snip20210220_26.png
  • LLDB控制台分析结果如下所示:
\color{red}{当断点停在第17行,YYPerson类/实例对象未调用方法,此时控制台打印结果如下:}
Snip20210220_27.png
  • 由objc_class结构体知道,cache成员前面存在isa成员与superClass成员,所以cache成员通过类首地址偏移16个字节得到cache的首地址;
  • YYPerson类/实例对象未调用方法,cache --> _buckets,_buckets中的元素bucket的成员_sel与_imp都是空的;
  • cache --> _occupied 自增量从0开始计数;
\color{red}{调用walk方法,断点停在第18行,此时控制台打印结果如下:}
Snip20210220_29.png Snip20210220_30.png
  • cache --> _occupied 自增量等于1;
  • YYPerson实例对象调用walk方法之后,cache --> _buckets,_buckets的成员_sel与_imp都是有值的;
  • 通过打印_sel与_imp,能看到walk方法, 将walk方的sel与imp赋值给_bucket结构体,然后将_bucket结构体存储到cache的_buckets哈希表中;
\color{red}{接着调用eat方法,断点停在第19行,此时控制台打印结果如下:}
Snip20210220_31.png Snip20210220_32.png
  • cache --> _occupied 自增量等于2;
  • p *$9 默认获取的是_buckets哈希表中第一个_bucket_t结构体,内部成员存储的是walk方法;
  • p *($9+1) 获取的是_buckets哈希表中第二个_bucket_t结构体,内部成员存储的是eat方法;
\color{red}{接着调用say方法,断点停在第20行,此时控制台打印结果如下:}
Snip20210220_33.png Snip20210220_34.png
  • cache --> _occupied 自增量等于1,为什么又等于1了???
  • p *$16 _bucket_t内部居然没值???
  • p *($16+1) _bucket_t内部有值,缓存的是say方法的sel和imp;
  • 先前缓存的walk与eat方法,放在哪里了???
  • 带着以上的疑问我们来进一步分析cache_t缓存的基本原理:
  • 在cache_t结构体中定义这样一个函数incrementOccupied(),此函数是实现_occupied成员变量自增的,其含义可以理解为用来记录cache_t的buckets哈希表中缓存方法的个数
Snip20210222_38.png
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  • 全局搜索incrementOccupied()函数发现其在cache_t的insert方法中被调用;
Snip20210222_39.png
  • insert函数实现的是将指定的方法的sel与imp存储到cache中;
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());
    
    //_occupied 自增量从0开始;
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    //当自增量=0时,创建缓存空间
    if (slowpath(isConstantEmptyCache())) {
        //capacity = 4(1<<2 => 100)
        if (!capacity) capacity = INIT_CACHE_SIZE;
        //开辟新的缓存空间,释放旧的缓存空间
        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.
        printf("xxxxx");
    }
    //存储空间两倍扩容,会清除先前的存储空间,然后开辟新的存储空间
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    //b -- 可看成是缓存bucket结构体的哈希表
    bucket_t *b = buckets();
    //掩码 (buckets容量 - 1) 可保证哈希函数计算出来的哈希索引值 不会出现越界
    mask_t m = capacity - 1;
    //生成插入哈希表中的 哈希索引值
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        if (b[i].sel() == sel) {
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}
  • 下面是insert方法的内部实现逻辑:
    • 类的cache在初始化时,_occupied = 0,从0开始;
    • 当_occupied=0表明类的cache未初始化,需初始化开辟缓存空间,默认开辟存储4个方法的内存空间;
    • 当(newOccupied + CACHE_END_MARKER) <= 3/4的缓存空间时,不做任何操作,其中CACHE_END_MARKER=1固定值,newOccupied是(_occupied+1);
    • 当(newOccupied + CACHE_END_MARKER) > 3/4的缓存空间时,会清除先前存储的所有方法,并在原来存储空间的基础上扩容两倍,即会新开辟8个存储空间,以此类推当进行第三次扩容时,会清除先前存储的所有方法,并在原来存储空间的基础上扩容两倍,即会新开辟16个存储空间
  • bucket_t结构体是用来存储方法的sel-imp;
  • 将存储方法的sel-imp键值对的bucket_t结构体插入buckets哈希表中的逻辑如下所示:
    • 首先调用cache_hash(sel, m)哈希函数,根据sel与mask计算出插入的哈希索引值
    • 若当前index位置的bucket_t的sel为0时,即没有插入任何元素,则插入存储目标方法的sel-imp的bucket_t结构体,然后直接结束循环;
    • 若当前index中有sel且与目标方法的sel相等,表明已经存储过该方法了,然后直接结束循环;
    • 上面循环体执行完成,没有退出循环,表明当前索引位置已经存储了其他方法,然后再移动到下一个位置(i+1),利用cache_next()函数再生成一个哈希索引,再执行循环体,有可能遍历整个哈希表;
  • 根据insert的源码我们知道开辟cache空间的函数为reallocate();
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = 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);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}
  • allocateBuckets(newCapacity)开辟新的存储空间;
  • setBucketsAndMask(newBuckets, newCapacity - 1) 将开辟的新的存储空间与cache绑定;
  • cache_collect_free(oldBuckets, oldCapacity) 清除先前旧的存储空间;
  • 下面再通过一个实例代码来验证上面的阐述:
@interface YYPerson : NSObject

@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)NSInteger weight;

- (void)code1;
- (void)code2;
- (void)code3;
- (void)code4;
- (void)code5;
- (void)code6;
- (void)code7;
- (void)code8;

+ (void)sing;
+ (void)work;

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //_occupied=0
        YYPerson *person = [YYPerson alloc];
        Class cls = [YYPerson class];
        //_occupied=1/capacity=4
        [person code1];
        //_occupied=2/capacity=4
        [person code2];
        
        ///cache空间2倍扩容,先前cache的方法被清除了,_occupied=1/capacity=8
        [person code3];
        
        //_occupied=2/capacity=8
        [person code1];
        //_occupied=3/capacity=8
        [person code2];
        //_occupied=4/capacity=8
        [person code4];
        //_occupied=5/capacity=8
        [person code5];
        
        ///_occupied=1/capacity=16 再次扩容
        [person code6];
        
        //_occupied=2/capacity=16
        [person code7];
        //_occupied=3/capacity=16
        [person code8];
        
        [YYPerson sing];
        [YYPerson work];
        
        NSLog(@" class = %@",NSStringFromClass(cls));
    }
    return 0;
}

\color{red}{当前断点停在[person code3]所在行} LLDB调试结果如下:

Snip20210222_40.png
  • _occupied = 2表明cache已经缓存了两个方法分别为code1与code2,自行验证;

\color{red}{过掉[person code3]的断点,停在[person code1]所在行} LLDB调试结果如下:

Snip20210222_41.png
  • [YYPerson code3]执行完之后,当(newOccupied + CACHE_END_MARKER) > 3/4的缓存空间(4),会进行空间的两倍扩容,开辟新的存储空间,空间大小为8,切会清除旧的存储空间即code1与code2被清除掉,然后再新开辟的存储空间中存储code3,此时的_occupied = 1;
    \color{red}{过掉[person code1]的断点,停在[person code6]所在行} LLDB调试结果如下:
Snip20210222_42.png
  • _occupied = 5 表明cache已经缓存了5个方法,分别为code1~5,由于哈希算法计算存储下标的随机性导致缓存方法之间的地址不连续;
    \color{red}{过掉[person code6]的断点,停在[person code7]所在行} LLDB调试结果如下:
Snip20210222_44.png
  • [YYPerson code6]执行完之后,当(newOccupied + CACHE_END_MARKER) > 3/4的缓存空间(8),会进行空间的两倍扩容,开辟新的存储空间,空间大小为16,且会清除旧的存储空间即code1~5被清除掉,然后再新开辟的存储空间中存储code6,此时的_occupied = 1;
  • 总结:
    • cache初始化时,_occupied=0即从0开始,Buckets哈希表默认开辟存储4个方法的容量;
    • 当cache缓存方法时,首先会判断(newOccupied + CACHE_END_MARKER)是否大于缓存空间的3/4,
      • 若不满足没有额外逻辑,直接存储目标方法;
      • 若满足条件,则会进行cache空间扩容,开辟新的且是原来cache空间两倍大小的存储空间,清除原来旧的存储空间,在新的空间中缓存目标方法,此时的_occupied=1;

相关文章

网友评论

      本文标题:iOS底层系列11 -- 类的cache成员分析

      本文链接:https://www.haomeiwen.com/subject/uniaxltx.html