美文网首页
iOS底层探索--cache

iOS底层探索--cache

作者: spyn_n | 来源:发表于2021-10-03 23:44 被阅读0次

      在前面的篇章我们探索了类的底层isa走位图class_data_bits_t bits数据,那么这个篇章我们一起探索cache_t cache类的缓存。带着以下几个问题,一起走进cache探索之旅。

    • cache到底是什么,是存什么数据?
    • cache的设计有何意义?
    • cache_t是什么结构?
    • cache是如何缓存数据的?

    iOS底层探索--方法慢速查找 这个篇章在方法的慢速查找之后有一部分cache的insert,方法插入缓存段
    iOS底层探索--方法快速查找(汇编)流程&慢速查找初探 这个篇章可以从汇编快速的查找到SEL的IMP。

    一、cache到底是什么,是存什么数据?

      以上这两个篇章的内容就可以解释cache到底是什么,是苹果对于方法调用的一种缓存机制,直接缓存方法的IMP与SEL的对应关系,以哈希表的形式存储起来,对SEL进行哈希,遇到冲突再哈希,将其值做了Key,(SEL+IMP也就是bucket)作为值,缓存起来,存的就是IMP和SEL。

    二、cache的设计有何意义?
    1. 因为方法的调用最后都转变成了消息的发送objc_msgSend,会给某个对象发送一条sel的消息,采用一对一的哈希键值可以快速的找到方法的实现。
    2. 方法的快速查找(汇编通过寄存器查找)可以快速的找到方法的IMP
    3. 采用缓存就不用每次都去遍历类的方法列表查找方法的实现,当类存在多个分类的时候,效率更加慢。
      总结一点:一切都是为了性能、效率。
    三、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;
        };
    }
    
    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
    
    cache_t结构
    四、 cache是如何缓存数据的?

    这实际上就是要看cache是如何建立起来的,方法是如何插入的:首先查看源码时可能会涉及到memory_order_relaxedmemory_order_release等可以查阅这篇文章 内存顺序(Memory Order)
    cache的insert流程:
    cls->cache.insert(sel, imp, receiver);

    1. 检测类的初始化标志位,如果没有初始化就什么不干
    2. 拿到newOccupied,oldCapacity,capacity初始值=oldCapacity
    3. 判断是否第一次申请缓存空间还是需要扩容
      1. 判断如果是空缓存,如果是就申请缓存空间reallocate(oldCapacity, capacity, /* freeOld */false); 创建容量为capacity大小(INIT_CACHE_SIZE,即是1<<2 = 4)的空间
        • 获取旧的buckets(根据第三个参数为true就进行释放旧值)
        • 调用allocateBuckets(newCapacity);创建一个capacity(4个)大小容量的buckets容器(以下说明创建的时候最后一个bucket_t就已经被占用了作为一个标记)
          • calloc(bytesForCapacity(newCapacity), 1);创建容器
          • 获取到最后一个bucket_t指针end
          • 设置最后一个bucket_t的SEL为1,IMP指向容器的首地址,也就是第一个bucket_t
          • 检测环境变量OBJC_PRINT_CACHE_SETUP为true,打印缓存容器
        • setBucketsAndMask(newBuckets, newCapacity - 1);设置的时候排除最后一个结束标志位固定占用的大小
          • _occupied初始化为0
        • 如果是扩容也就是freeOld=true则collect_free(oldBuckets, oldCapacity)
      2. 如果有缓存空间了,新的占用量newOccupied + 1个结束标记bucket <= 缓存填充比例3/4.
      3. 如果达到扩容条件(也就是大于缓存填充比例),就进行2倍扩容,但是capacity最大不超过2^16=65536,重新调用reallocate(oldCapacity, capacity, true);创建更大的容器,并且把旧的bucktes释放,也就是把所有的bucket_t丢弃。(为什么丢弃:因为将旧数据挪到新的buckets的CPU开销成本太大)
    4. 创建一个新的bucket_t b = buckets(), 对sel进行hash ---> (sel & (capacity-1))得到一个下标
    5. 进行do...while循环,查找一个未使用的插槽并插入
      • 以sel的hash值为key,从bucket中取出sel,
        • 如果不存在_occupied++,给bucket set sel和IMP set(b,sel, imp, cls()),设置的时候根据是都原子和encode对IMP进行编码再存储
        • 如果sel已经存在,说明已存在缓存,则什么也不干直接退出
      • 条件cache_next(i, (capacity - 1)) --> (i + 1) & (capacity - 1) != begin也就是进行在哈希
    6. 如果插入不成功,就bad_cache

    相关文章

      网友评论

          本文标题:iOS底层探索--cache

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