iOS底层原理之方法缓存

130 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,  点击查看活动详情

前言

前面说过实例方法放在了中,而非对象中,因为一个类创建出多个对象,放在如果将实例方法放在对象中,会造成内存空间的浪费。今天要探索的实例方法缓存也放在了中,便是cache_t cache进行方法缓存。前面探索了类的数据存储 class_data_bits_t,是拿到isa地址后平移了32位获取到,cache_t cache也是通过平移的方式获取。

方法缓存内部分析

首先打开objc.dylib.A源码库,定位到objc-runtime-new类中的struct objc_class : objc_object结构体,今天要探索的便是cache_t,点击进去查看实现,该结构体实现比较长,这里摘抄了(目前探索到的)比较重要的属性和方法来分析

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };

        explicit_atomic<preopt_cache_t *> _originalPreoptCache;

    };
    // 省略一些代码
    ...
    
    void incrementOccupied();

    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);


    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);

    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);



    static bucket_t *emptyBuckets();

    static bucket_t *allocateBuckets(mask_t newCapacity);
    
    // 省略一些代码
    ...
    struct bucket_t *buckets() const;
    
    // 省略一些代码
    ...
    
    void insert(SEL sel, IMP imp, id receiver);
  1. _bucketsAndMaybeMask: 存储了buckts的地址,在setBucketsAndMask()函数中可以看到

image.png

  1. _maybeMask:掩码,该值来至于开辟的缓存的容积 _flags:64位系统下标识, _occupied: 占位;
  2. struct bucket_t *buckets() 函数最终返回的是一个bucket_t 类型的结构体指针,内部实现如下图,
    • 上面说过通过_bucketsAndMaybeMask存储了buckts的地址,所以这里通过_bucketsAndMaybeMask获取到buckets地址
    • 获取到bucket地址不纯,需要和bucketsMask掩码进行&操作

image.png

bucket_t内部实现如下图,可以看到bucket_t内部提供了selimp属性,以及获取selimp的方法

image.png

image.png struct bucket_t *buckets()小结:我们通过调用buckets()函数获取到bucket_t地址,有了地址通过bucket_t提供的sel()和imp()函数就可以获取到缓存的方法了

  1. 方法缓存必然要经历一个插入方法的过程,这里看到了cache_t的核心方法insert(SEL sel, IMP imp, id receiver),传入sel imp receiver 进行方法缓存

insert(SEL sel, IMP imp, id receiver)分析

insert()部分代码

void cache_t::insert(SEL sel, IMP imp, id receiver)

{

    // Use the cache as-is if until we exceed our expected fill ratio.

    mask_t newOccupied = occupied() + 1; // 没有属性赋值的情况下occupied = 0,newOccupied = 1

    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    

    if (slowpath(isConstantEmptyCache())) {

        // Cache is read-only. Replace it.

        if (!capacity) capacity = INIT_CACHE_SIZE; // 初始化 capacity = 1 << 2 = 4

        reallocate(oldCapacity, capacity, /* freeOld */false);

        // 到目前为止,if的流程的操作都是初始化的创建
    }

    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.

        // 如果 小于等于 占用内存的 3/4,什么都不用做

        // 第一次使用时,申请开辟4个内存,如果此时已经有了3个从bucket插入到cachet里面,在插入1个就是4个,就数组越界,所以需要在原来的容量上进行两倍的扩容

    }

    else {

        // 如果超出了3/4,则需要扩容

        // 扩容算法: capacity 有值,扩容两倍,无值则初始化为4

        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;

        if (capacity > MAX_CACHE_SIZE) {

            capacity = MAX_CACHE_SIZE;

        }

        reallocate(oldCapacity, capacity, true);

    }


    bucket_t *b = buckets();

    mask_t m = capacity - 1; // 因为最后一位用来站位了,一个空的bucket

    mask_t begin = cache_hash(sel, m);// 哈希下标--通过哈希函数计算sel存储的下标

    mask_t i = begin;


    // Scan for the first unused slot and insert there.

    // There is guaranteed to be an empty slot.

    do {

        // 如果当前遍历的下标拿不到sel,即表示当前下标没有存储sel

        if (fastpath(b[i].sel() == 0)) {

            // 则将sel存储进去,并将对应的occupied加加

            incrementOccupied();

            b[i].set<Atomic, Encoded>(b, sel, imp, cls());

            return;

        }

        // 如果当前哈希下标的sel等于准备插入的sel,则直接返回,说明已经缓存过了

        if (b[i].sel() == sel) {

            // The entry was added to the cache by some other thread

            // before we grabbed the cacheUpdateLock.

            return;

        }

        // 如果当前计算的哈希下标已经存储了sel,且两个sel不相等,需要重新进项哈希计算 等到新的下标

    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);

}

第一次调用方法进行缓存时:

  1. mask_t newOccupied = occupied() + 1; occupied表示占位,第一次调用时occupied()等于0,所以此时newOccupied = 1
  2. isConstantEmptyCache()函数是成立,因为第一次调用没有缓存的方法。
  • capacity = 1 << 2,1 左移两位变成了100,那就是capacity = 4capacity表示容积

  • reallocate(oldCapacity, capacity, freeOld);开辟缓存空间,参数传递:曾经开辟空间大小,要开辟空间的大小,是否释放曾经开辟的空间

    image.png 非第一次调用方法进行缓存时:

  • fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)) 当前占位小于等于容积的75%时,什么都不需要做,看下图

image.png

  • 如果超过了75%则需要扩容
    • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; 如果capacity有值则进行2倍扩容,如果没有,则初始化为4
    • if(capacity > MAX_CACHE_SIZE) capacity = MAX_CACHE_SIZE;如果扩容后容积大于1 << 16,则强制将容积等于最大值,即capacity = 1 << 16
    • reallocate(oldCapacity, capacity, true); 根据传递的capacity大小重新开辟内存空间,并且释放oldCapacity
  • bucket_t *b = buckets(); 获取bucket_t结构体指针
  • mask_t m = capacity - 1; 为了防止越界,最后一位用来占位了,是一个空的bucket,m 是容积减1,在下面哈希过程中把它当成了掩码
  • mask_t i = cache_hash(sel, m); 通过哈希算法计算sel对应的下标。

image.png

  • 寻找到正确的位置,将sel和imp存入到bucket_t
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_t

有的时候因为源码无法运行,或者LLDB调试过于麻烦,这时就想脱离源码进行分析,整个过程就是将源码中类的数据结构和自己定义类的结构保持对应即可,也就是将类的数据对应到自定义的类的结构中。

  1. 创建一个自己的项目,将源码中类的数据结构拷贝过来,只要属性即可,方法不占空间,不需要对应

源码类的内存结构: image.png

在写自定义类的内存结构时

  • 直接将继承至objc_objectClass isa写在类结构中,删除继承objc_object
  • 为了和系统的cache_t 区别开,这里自定义了dxj_cache_t结构体
    • 系统的cache_t image.png
    • 对于结构体和联合体已经很熟悉了,因为联合体是互斥的,所以只保留了联合体内部的结构体,结构体内部共用的,所以结构体套结构体,可以直接将内部结构体提取到外部,最后自定义的dxj_cache_t image.png
    • 上方分析过_bucketsAndMaybeMask,内部其实就是存储了bucket_t *这个结构体指针,并且bucket_t内部就是sel 和 imp,所以这里我们直接将_bucketsAndMaybeMask替换为bucket_t *类型的数据,如下图 image.png
  • 为了和系统的class_data_bits_t 区别开,这里自定义了dxj_class_data_bits_t结构体 image.png

自定义类的内存结构: image.png

使用:

image.png

lldb查看方法缓存

  1. 源码环境,当还没有调用DXJTeacher的任何方法时,_maybeMask_occupied 都为0,也就是占位为0也没有开辟缓存空间 image.png
  2. 当我们调用[t sayHello]时,_maybeMask开辟了4个空间(这里显示为3,因为最后一个是空桶子,作为末尾标识,能用的就3个),_occupied 占位为1,表示桶子中插入了一个缓存方法。

image.png 3. _bucketsAndMaybeMask上方分析过了里面存储了bucket_t *指针地址,所以将_bucketsAndMaybeMask地址强转为bucket_t *类型

image.png 4. 通过平移bucket_t *获取内部bucket_tsel 和 imp

image.png