iOS底层原理-cache_t分析(上)

367 阅读5分钟

前言

在前几篇文章对类的结构进行了初步的探索,研究了objc_class的内部结构,以及上一篇先跳过了cache_t,而通过内存平移对class_data_bits_t的结构进行了分析,了解了bits存储在class_rw_t内的methodspropertiesprotocols等数据,这篇着重了解一下cache_t的内部结构,也就是类缓存里存放的什么数据?以及如何进行缓存的?

cache_t的结构图

cache_t代码

在了解cache_t的结构之前,还是把源码中的结构代码复制过来,来了解cache的内部构造,代码如下:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4字节
#if __LP64__
            uint16_t                   _flags;  // 2字节
#endif
            uint16_t                   _occupied; // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
    };
}

结构图

根据上面代码结构我们可以简单的画个结构图,如下: 005.png cache_t里储存了_buckets_mask_flags_occupied等信息,_buckets里就存放了selimp,接下来我们通过代码验证一下cache_t的内存结构。

LLDB调试

由上面的cache_t的代码结构,我们得出了一个粗略的结构图,现在我们通过lldb一步步调试了看看获取到的数据。 006.png 首先通过p/x LGPerson.clas拿到LGPerson的首地址0x0000000100008818,然后内存平移16字节,也就是加上0x10得到0x0000000100008828,打印输出cache_t结构的$1,通过*$1输出cache_t的数据,从结果来看,得到的数据和上面的代码定义的一致,继续获取_bucketsAndMaybeMask_maybeMask_originalPreoptCache,看看输出什么?

007.png

通过打印输出发现获取不到对应的值,也没有看到相关的selimp,再次去查找源码发现cache_t结构中有insert方法,而在insert方法中包含了对bucket的创建,接着我们通过bucket的类型bucket_t找到下面的定义:

// 不同架构储存sel和imp的顺序不一致
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
}

接下来我们通过调用buckets()方法,看看输出

008.png 可以看到输出了selimp,但值是nil,原因是我们没有调用方法,继续调用方法,然后lldb重新开始跑一遍(需要更新内存数据),再打印输出

009.png

通过输出可以看到_maybeMask已经有值了,说明上面调用[p say1]方法已经成功了,但我们输出的selimp还是为空,这是什么原因?

010.png 通过打印buckets()[1]可以看到selimp是有值的,这里buckets有个概念是,它是哈希函数buckets里存的是多个bucket,而哈希函数并不是从0开始存储的,是根据函数定义存储的,所以我们在1号位置有输出,关于哈希函数相关知识点可以另行查阅,本篇不做过多解释;下面通过调用sel()可以看到方法的输出,调用imp()报错是由于缺少参数。

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)

源码分析

结合上面的lldb调试,发现调用方法后需要重新获取类的内存,然后一步一步执行获取cache_t数据的操作,不方便调试,而且无法在没有源码的项目中进行调试,鉴于这个原因,我们可以按照源码的设计自定义一个objc_classcache_t

自定义cache结构

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

// bucket_t
struct kc_bucket_t {
    SEL _sel;
    IMP _imp;
};

// cache_t
struct kc_cache_t {
    struct kc_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

// class_data_bits_t
struct kc_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;
    struct kc_class_data_bits_t bits;
};

接下来我们在main函数里执行

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;
        [p say1];

        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
        
        for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
            struct kc_bucket_t bucket = kc_class->cache._bukets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
    }
    return 0;
}

代码运行

先只调用一个方法[p say1]看一下结果

011.png 输出了say1方法,并且下面输出2个(null) - 0x0f,说明buckets开辟了3个容量,存了say1方法,另外2个空着,_occupied = 1_maybeMask = 3,接下来调用3个方法看一下结果

013.png 可以看到打印了3个方法,但输出的只有say3_maybeMask由原来的3增加到7,为什么会出这样的结果?接下来还是去源码中看看具体逻辑。

insert源码

首先看一下cache_tinsert()方法的部分源码(由于比较长,过滤掉一些判断条件和断言)

// Historical fill ratio of 75% (since the new objc runtime was introduced).
// 设置3/4的容量值,当超过这个限定值就扩容
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
// 给buckets开辟内存
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    // 如果是扩容需要回收之前buckets的内存
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // 初始化 occupied() = _occupied + 1; 首次是 0+1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    
    // 如果是首次cache为空,会进入当前判断条件
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        // 给capacity赋值,INIT_CACHE_SIZE: 1 << 2 = 4 capacity首次等于4
        if (!capacity) capacity = INIT_CACHE_SIZE;
        
        // 给bucket按照capacity容量开辟内存
        reallocate(oldCapacity, capacity, /* freeOld */false); 
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
        // 当bucket存储容量+1 <= 75%空间不作处理,下次继续插入
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        // 当bucket插入超过3/4时进行原来的2倍扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        // 再次分配内存给bucket
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4 - 1 = 3
    mask_t begin = cache_hash(sel, m); // 哈希函数(sel & m)
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    循环判断当前bucket的sel是否在cache里存在,如果没有做插入,如果有不做处理
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, 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_next():哈希函数,对比和begin是否相等,不一致继续下一次循环

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

总结

从源码insert()方法我们可以看到类中的方法会去查找cache里有没有对应的方法,如果有就直接从cache里获取,如果没有就插入到缓存中,所有的selimp都是存储在cache_t结构的buckets里,给buckets开辟内存空间时有个3/4容量的界定值的概念,当buckets超出这个值就会以2倍的capacity扩容,但在setBucketsAndMaskcapacity - 1的,这就是上面例子中,输出的1 - 31 - 7的原因,而在第二次调用say1、say2、say3三个方法时为什么只输出say3一个,其他都是(null) - 0x0f,是由于在调用say3时进行了扩容,回收了之前的buckets内存空间。

以上就是通过一个示例再结合源码对cache_t进行的分析探索过程。