iOS-12.cache_t结构分析

581 阅读6分钟

ios底层文章汇总

1.objc_class结构的成员构成

iOS-Objc类结构解析中可知类结构objc_class偏移16个字节,可以得到cache_t cache成员变量,cache中缓存方法(即sel很imp)

objc_class结构的成员构成如下

2.cache_t源码查看


struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//模拟器(x86_64)和macos(i386)
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//真机(arm64)
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // mask占用的位数:16位,除去mask其余字段占用位数:64-16=48位(maskZeroBits=4,bucket=44,)
    static constexpr uintptr_t maskShift = 48;
    
    //  在mask和bucket中间额外预留4位,必须为0
    static constexpr uintptr_t maskZeroBits = 4;
    
    // 存储的mask的最大值
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // bucket的遮罩 : bucket = _maskAndBuckets & bucketsMask;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // 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 //真机(32位)
    // _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;

    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;

public:
   
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    unsigned capacity();
    //....

从源码可知,objc4-781根据不同的架构,对cache_t定义不同. 架构分析 macos : i386 模拟器 : x86 真机 : arm64

CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED : 模拟器和macos

CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 : 真机(arm64)

CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 : 真机(32位)

3.bucket源码查看

struct bucket_t {
private:
#if __arm64__ //真机
    //explicit_atomic 原子保护
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else //非真机
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
    //....
}

3.lldb调试获取cache_t和bucket内容

3.1 定义类


@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end


@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

3.2 使用类

1. LGPerson *p  = [LGPerson alloc];
2. Class pClass = [LGPerson class];
3. [p sayHello];
4. [p sayCode];
5. [p sayMaster];

3.3 通过lldb命令可以获取到cache_t的内容和结构

将断点设置在5行位置,可以进行lldb命令打印获得cache_t的内容如下图:

通过machoView打开target的可执行文件,在方法列表中查看其imp的值是否是一致的,如下所示,发现是一致的,所以打印的这个sel-imp就是LGPerson的实例方法

4.cache的写入流程

4.1 探寻思路:

    1. cache中的_occupied会随着方法的调用而递增,由此我们可以探寻到_occupied增加的函数incrementOccupied,其源码实现如下:
void cache_t::incrementOccupied() 
{
    _occupied++;
}
    1. 全局搜索incrementOccupied ,找到有且仅有一处调用,cache_t::insert
    1. 在cache_t::insert中设置断点,可以找到insert的调用位置在 cache_fill,而cache_fill的调用位置在lookupMethodInClassAndLoadCache

总结: 由上述探索,初步可以得出cache的写入流程如下:

方法调用-->编译生成objc_msgSend-->根据类和sel传入lookUpImpOrForward函数中,查找imp-->快速查找-->快速查找没有找到imp,进入慢速查找,遍历方法列表-->找到imp后,cache_fill-->cache_t::insert-->修改cache_t内容

注意:_occupied修改值的情况

  • 调用init方法,_occupied 会+1

  • 当有属性赋值调用set方法,_occupied会+1

  • 当有方法调用时,_occupied也会增加

  • 重新分配内存,_occupied = 0归零

4.2 insert的执行逻辑

cache_t.png

4.3 cache内存分配逻辑

objc底层cache_t::insert的部分源码如下:

 mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        if (!capacity) capacity = INIT_CACHE_SIZE;//INIT_CACHE_SIZE=4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // CACHE_END_MARKER= 1
        //newOccupied+1<=capacity / 4 * 3 不处理
    }
    else {// newOccupied+1 > capacity / 4 * 3 需要扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 :INIT_CACHE_SIZE=4
        if (capacity > MAX_CACHE_SIZE) { //MAX_CACHE_SIZE<<16,最多缓存MAX_CACHE_SIZE个bucket,当occupied占用到MAX_CACHE_SIZE*3/4个时会重新申请缓存,释放原来的空间,将原来的缓存丢弃
            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;

    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时,_occupied = 0并且isConstantEmptyCache = true,向系统申请4个bucket的内存空间

  • 每次在底层调用objc_msgSend发送的消息,则会优先查找cache里的bucket,如果cache里没找到对应的imp,则进入慢速查找方法列表,找到后调用insert方法

  • newOccupied+1<=capacity / 4 * 3 ,直接执行bucket插入

  • newOccupied+1 > capacity / 4 * 3 ,第一次扩容重新申请内存是第3次进入insert方法时,需要扩容到capacity*2 个bucket的内存空间,同时释放原来的内存,并将_occupied归零(_occupied = 0)

  • 扩容最大可以到MAX_CACHE_SIZE= 32,最多缓存32个bucket,

  • capacity =MAX_CACHE_SIZE = 32时 当occupied占用到32*3/4=24个时会重新申请内存,释放原来的空间,将原来的缓存丢弃

4.4 新的bucket插入的逻辑

//获取buckets的首地址
bucket_t *b = buckets();
// mask = capacity - 1;
mask_t m = capacity - 1;
//cache_hash计算(sel & mask)首次尝试插入bucket存储的index位置,如果index位置被占用需要重新计算插入位置
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//遍历buckets的地址空间,cache_next循环找一个位置存储新的bucket
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        if (b[i].sel() == sel) {
            //该位置被占用,且存储的bucket的sel就是我们需要缓存的sel,则不需要再重复存储,这种情况可能出现在多线程异步的情况下
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));
    
  • 获取buckets的首地址,计算mask (mask = capacity - 1)

  • cache_hash计算(sel & mask)首次尝试插入bucket存储的index位置,如果index位置被占用需要重新计算插入位置

  • do-while循环(cache_next(i, m))查找buckets中一个没有被占用的位置,存储需要缓存的bucket; 找到空位存储退出循环 或者 找到已经缓存的bucket退出循环

其中cache_next的算法如下:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask; //(将当前的哈希下标 +1) & mask,重新进行哈希计算,得到一个新的下标
}

4.5 真机下重新申请内存时,需要将cache的_maskAndBuckets更新(调用setBucketsAndMask),

为了优化内存,arm64的真机下mask值和buckets的地址共同占用64位的空间,在内存中存储位域如下图

所以我们在缓存buckets时也需要进行一些位移操作,具体实现如下:

5. 脱离源码探索cache_t

在已知底层结构的情况,可以将底层的objc_class,cache_t,bucket_t结构仿照源码格式和类型自定义一个相似的结构体,然后将类强转为我们自定义的结构,就可清晰的看到底层结构变换过程

5.1 准备类和调用源码

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

- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;

+ (void)sayHappy;

@end


@implementation LGPerson

- (void)say1{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say2{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say3{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say4{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say5{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say6{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say7{
    NSLog(@"LGPerson say : %s",__func__);
}


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

  • 自定义结构体objc_class,cache_t,bucket_t,和调用代码
#import "LGPerson.h"
#import <objc/runtime.h>

typedef uint32_t mask_t;

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; //注意objc_class这个位置有个isa
    Class superclass;
    struct lg_cache_t cache;
    struct lg_class_data_bits_t bits;
};


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
               NSLog(@"(1)_occupied: %hu - _mask:%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);
               }

        [p say3];
        NSLog(@"(2)_occupied: %hu - _mask:%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);
        }
        [p say4];
         

       NSLog(@"(3) _occupied: %hu - _mask:%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);
              }
        [p say1];
        [p say2];
        NSLog(@"(4) _occupied: %hu - _mask:%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);
               }

        
        NSLog(@"Hello, World!");
    }
    return 0;
}

5.2 打印输出解析

  • _occupied : 缓存的buckets被使用(存储bucket)的个数,也就是cache中缓存的方法个数,对象创建时为0,重新申请内存后会清零,添加一个bucket后会_occupied++

  • _mask : 申请的buckets总容量-1(可存储bucket个数-1),首次申请4个bucket内存,此时_mask =4-1=3;

  • 重新申请buckets内存,原来的内存会清空,_occupied清零,cache中原缓存的bucket也会丢弃

  • 新的bucket插入顺序,不是连续的,根据cache_hash计算起始查询位置,循环cache_next查找一个空位置存储