美文网首页
cache_t底层分析

cache_t底层分析

作者: 镜像 | 来源:发表于2021-06-29 19:11 被阅读0次

    类的本质我们已经分析完了,里面有isasuperclasscachebits
    今天对cache进行研究。在objc源码中可以看到cache的类型是cache_t,用lldb调试打印cache的所有数据:

    cache_t

    在这里看的数据会感觉比较混乱,那我们从源码中分析,最后再回来验证。
    struct cache_t结构中,我们根据提供的方法来分析里面的数据结构,找到一行关键代码void insert(SEL sel, IMP imp, id receiver);,缓存中插入数据,根据参数我们一下就能知道cache里面缓存的是方法了。点击insert方法看里面都有哪些操作:

    image

    发现里面有bucket_t这个数据结构,方法就插入到这里面,再看下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里面就有两个成员,一个_imp,一个_sel,这下我们心里大概能有个概念,cache是缓存方法的,里面有个bucket_t数组,数组每一项里面存着impsel

    虽然得出了一些结论,但是还是要通过lldb进行验证。因为cache_t里面没有bucket_t这个成员,所以就要找相关方法看有没有能获取的,正好找到struct bucket_t *buckets() const;这个方法可以获取,那我们进行验证。

    bucket_t

    输出发现是有bucket_t,但是里面的值都是空的,怎么回事呢?以为缓存是缓存的方法,你什么方法都不执行哪来的缓存,我们重新执行下代码。

    SJPerson *p = [SJPerson alloc];
    [p doSomething];
    
    image
    image

    我们看到上面Value是3,打印出来为什么还是没有数据呢?那我们试图打印buckets后面几个数据试试:

    image

    发现第2个元素有值了,为了验证是不是我们的doSomething方法,在bucket_t结构里面找对应的输出方法,继续调试

    image

    发现确实是我们的方法,没有问题。
    接下来我们对cache_t进行原理分析。既然是缓存,肯定有读写相应的操作,我们从写作为入口进行分析,为什么不从读作为入口呢,因为你不先写,读也读不出来数据。在cache_t中的方法void insert(SEL sel, IMP imp, id receiver);,我们点进去看下插入都做了什么操作。

    void cache_t::insert(SEL sel, IMP imp, id receiver)
    {
        /// occupied(): return _occupied; 成员变量,默认0
        mask_t newOccupied = occupied() + 1; // 这里+1是为了把这次执行的方法先加上
        unsigned oldCapacity = capacity(), capacity = oldCapacity;
        /// 第一个if:cache是空
        if (slowpath(isConstantEmptyCache())) {
            // Cache is read-only. Replace it.
            ///  capacity=0时 初始设置 capacity = INIT_CACHE_SIZE = 4,真机为2
            if (!capacity) capacity = INIT_CACHE_SIZE;
            /// 申请缓存的内存空间,具体代码在下一段
            reallocate(oldCapacity, capacity, /* freeOld */false);
        }
        /// 如果 newOccupied + 1 <= 3/4 * capacity,还可以继续插入,对桶子不做操作,真机的负载因子是7/8
        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.
        }
    /// arm64&&LP64 即真机环境下  CACHE_ALLOW_FULL_UTILIZATION为1
    #if CACHE_ALLOW_FULL_UTILIZATION
        /// 如果 capacity<=1<<3,即8 && newOccupied <= capacity,还可以继续插入,对桶子不做操作
        /// 真机容量小于等于8,可以全填满,大于8时真机用7/8
        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 {
            /// capacity为0时,capacity = 4,不为0时 capacity *2直接扩容到原来的2倍
            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; 
        /// 利用hash算法计算下标开始值,供下面循环使用,算法真机环境稍微有所区别
        mask_t begin = cache_hash(sel, m);
        mask_t i = begin;
    
        /// 一个循环,退出条件:1、成功插入  2、已经被插入  3、找一圈下标也没合适位置插入。3个条件满足一个即退出循环,最后一种情况会走bad_cache
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot.
        do {
            /// 如果桶子的i位置没有存数据,则直接插入
            if (fastpath(b[i].sel() == 0)) {
                /// _occupied++;  也就是说_occupied是记录方法插入的个数
                incrementOccupied();
                /// 插入方法,真机和非真机有小区别
                b[i].set<Atomic, Encoded>(b, sel, imp, cls());
                return;
            }
            /// 如果当前位置插入的有数据并且与执行的方法相同,直接return
            if (b[i].sel() == sel) {
                // The entry was added to the cache by some other thread
                // before we grabbed the cacheUpdateLock.
                return;
            }
          /// 当前位置有数据,获取下一个坐标,并且坐标不等于开始坐标,继续循环
          /// cache_next实现在下面
        } while (fastpath((i = cache_next(i, m)) != begin));
    
        bad_cache(receiver, (SEL)sel);
    #endif // !DEBUG_TASK_THREADS
    }
    
    /**
     * 模拟器,mac环境CACHE_END_MARKER为1
     * 负载因子3/4
     * CACHE_END_MARKER为1,buckets填充结束标志
    */ 
    #if __arm__  ||  __x86_64__  ||  __i386__
    #define CACHE_END_MARKER 1
    static inline mask_t cache_fill_ratio(mask_t capacity) {
        return capacity * 3 / 4;
    }
    #elif __arm64__ && !__LP64__
    #define CACHE_END_MARKER 0
    static inline mask_t cache_fill_ratio(mask_t capacity) {
        return capacity * 3 / 4;
    }
    /**
     * 真机环境 CACHE_END_MARKER为0
     * 负载因子7/8
     * CACHE_END_MARKER为0,buckets不用填充结束标志
     * CACHE_ALLOW_FULL_UTILIZATION为1,小容量缓存允许充满
    */ 
    #elif __arm64__ && __LP64__
    #define CACHE_END_MARKER 0
    static inline mask_t cache_fill_ratio(mask_t capacity) {
        return capacity * 7 / 8;
    }
    #define CACHE_ALLOW_FULL_UTILIZATION 1
    
    #else
    #error unknown architecture
    #endif
    
    
    enum {
    #if CACHE_END_MARKER || (__arm64__ && !__LP64__)
        INIT_CACHE_SIZE_LOG2 = 2,
    #else
        INIT_CACHE_SIZE_LOG2 = 1,
    #endif
        /// 真机环境INIT_CACHE_SIZE=2,模拟器mac INIT_CACHE_SIZE=4
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
        MAX_CACHE_SIZE_LOG2  = 16,
        MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
        FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
        FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
    };
    
    
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
    {
        /// 旧桶子
        bucket_t *oldBuckets = buckets();
        /// 新桶子根据newCapacity来申请,申请的具体代码在下一段
        bucket_t *newBuckets = allocateBuckets(newCapacity);
        /// 设置_bucketsAndMaybeMask成员变量
        setBucketsAndMask(newBuckets, newCapacity - 1);
        /// 释放旧桶子,也就是把原来的缓存数据丢掉不要了
        if (freeOld) {
            collect_free(oldBuckets, oldCapacity);
        }
    }
    
    
    #if CACHE_END_MARKER
    bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
    {
        // Allocate one extra bucket to mark the end of the list.
        // This can't overflow mask_t because newCapacity is a power of 2.
        /// 申请内存空间,大小:newCapacity
        bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
    
        /// 创建桶子最后一个元素,来标记buckets末尾
        bucket_t *end = endMarker(newBuckets, newCapacity);
    
        /// 官方注释也写的很明白,把最后一个元素设置上值,sel是1,
    #if __arm__
        // End marker's sel is 1 and imp points BEFORE the first bucket.
        // This saves an instruction in objc_msgSend.
        end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
    #else
        // End marker's sel is 1 and imp points to the first bucket.
        end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
    #endif
        /// 打印相关信息
        if (PrintCaches) recordNewCache(newCapacity);
    
        return newBuckets;
    }
    #else
    /// 真机会走这个方法,直接申请内存,没有插入endBucket
    bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
    {
        if (PrintCaches) recordNewCache(newCapacity);
    
        return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
    }
    
    #endif
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    /// 从这个方法代码可以看出_bucketsAndMaybeMask是存buckets。
    /// 这里有个小细节newMask是(newCapacity - 1),也就是总长度减1
    void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
    {
        _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
        _maybeMask.store(newMask, memory_order_release);
        _occupied = 0;
    }
    /// 这里又有小细节,mac环境mask直接用_maybeMask,和真机不一样哦,调试时会发现
    mask_t cache_t::mask() const
    {
        return _maybeMask.load(memory_order_relaxed);
    }
    
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 || CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    /// 真机会走这里~~
    /// 把mask和buckets存到_bucketsAndMaybeMask里面,基于位运算
    void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
    {
        uintptr_t buckets = (uintptr_t)newBuckets;
        uintptr_t mask = (uintptr_t)newMask;
    
        ASSERT(buckets <= bucketsMask);
        ASSERT(mask <= maxMask);
    
        _bucketsAndMaybeMask.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, memory_order_relaxed);
        _occupied = 0;
    }
    
    /// 真机的mask是根据_bucketsAndMaybeMask来的,这也是写类似结构体真机调试时_maybeMask一直为0的原因
    mask_t cache_t::mask() const
    {
        uintptr_t maskAndBuckets = _bucketsAndMaybeMask.load(memory_order_relaxed);
        return maskAndBuckets >> maskShift;
    }
    
    /// 真机是每次-1,当减到0时,设置为mask
    #if CACHE_END_MARKER
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    #elif __arm64__
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    

    上面代码关键内容我都进行了注释,里面有些需要注意的细节

    1. mac初始创建容量为4,真机环境初始创建容量为2,每次扩容是当前容量的2倍
    2. mac当_occupied+1大于3/4容量时进行扩容,为什么是3/4呢?因为负载因子0.75时,空间利用率是比较好的,和对hash冲突可以有效避免,具体的可查阅相关算法知识
    3. 真机当容量小于等于8的时候是可以填满buckets的,大于8时负载因子是7/8
    4. _maybeMask的值是容量减1
    5. mac环境_maybeMask直接可以读到值,真机时没有,只能用_bucketsAndMaybeMask做位运算获取
    6. hash算法解决冲突时,arm64与非arm64算法不同,真机是减1,减到0是等于mask
    7. buckets是散列表结构,单是注意不是数组!不是数组!!不是数组!!!强调3遍!!!
    8. 每次扩容申请新buckets时,都会把旧的释放掉
    9. mac环境buckets有个结束标志,新建时把最后一个bucket设置到buckets末尾,来标记这段散列表的结束,它的sel是1。真机是没有结束标志的。

    insert流程我们总结完了,是不是对cache_t的理解更深一步呢?什么??上面代码太乱???好吧,我就总结下insert的流程图吧,因为mac和真机流程不一样,所以这里总结两个流程图:

    cache_t insert-mac.jpg cache_t insert-真机.jpg

    insert流程总结完,我们看一个非常有意思的现象。创建SJPerson类,里面生命并实现say1say2方法:

    @interface SJPerson : NSObject
    
    - (void)say1;
    - (void)say2;
    
    @end
    
    @implementation SJPerson
    
    - (void)say1
    {
        NSLog(@"%s", __func__);
    }
    - (void)say2
    {
        NSLog(@"%s", __func__);
    }
    @end
    

    然后我们执行下面代码并打断点

    SJPerson *p = [SJPerson alloc];
    [p say1];
    

    执行完say1的时候,按上面的分析,maybeMask应该等于3,lldb进行调试验证:

    image

    验证没有问题,我们增加代码[p say2],并在say2后面打断点,按上面的分析,maybeMask应该等于3,lldb进行调试验证:

    image

    验证通过,接下来我们做一个骚操作,删除say2,在say1执行完后打断点,这时候我们已经验证过maybeMask是3,occupied是1,没有问题。这时我们在lldb中输入p [p say2],这时候也是让p执行say2方法,按道理执行完cache里面的结果应该跟上面一样,进行验证:

    image

    神奇的一幕出现了,maybeMask成7了,occupied还是2没问题。诶?7也就是说cache做了一次扩容,但是我们就执行了两个方法,初始容量是4,负载因子0.75,2小于3应该不会扩容啊,我们继续看这8个位置的方法都有什么。

    image

    缓存里面有classsay2两个方法,但是class我们并没有执行啊,这时候我们猜测在lldb调试执行p [p say2],系统会自动执行一些方法。在insert方法里面打印相关信息printf("cache_t insert ====== sel : %s imp : %p receiver : %p \n", (char *)sel, imp, receiver);

    image image

    我们看到p [p say2]时,插入了很多方法,我们只看receiverp对象的方法,有三个,这时候我们可以继续分析,[p say1]时候插入一个方法;要插入respondsToSelector时,计算3<=43/4,继续插入;插入class时,4>43/4,所以要扩容重新开辟内存,这是容量就变成8了,maybeMask为7;插入say2时,3<=8*3/4,插入,这时cache里面有两个方法,跟我们分析的一致了。

    真机验证insert

    我们先根据上面分析猜测一下真机验证的结果。我们画个猜测流程图:

    真机cache缓存流程猜测.jpg

    流程猜测完了,真机验证下是否和我们猜测结果一致呢?答案是一致,非常完美,附上测试demo的链接,可以下载测试。cache_t-Demo

    这里还有个坑点,我最早跑demo时候,结果和上面流程并不一样,初始容量4,负载因子一致是0.75,把源码看了一遍又一遍都怀疑人生了。后来发现是自己iOS版本太低,因为objc源码我下载最新的,但是iOS版本低,真机运行和分析结果会有所差别,所以在分析时,
    一定要下载最新的源码,iOS升级最新系统,xcode升级最新版本!!!
    一定要下载最新的源码,iOS升级最新系统,xcode升级最新版本!!!!!
    一定要下载最新的源码,iOS升级最新系统,xcode升级最新版本!!!!!!!
    重要的事情说3遍,我下载源码版本818.2,手机iphoneX,iOS14.6,xcode12.5。

    objc_msgSend引入

    缓存的插入我们已经了解了,那在什么时机会调用插入呢,我们继续研究。
    objc-cache.mm可以看到文件说明注释中写的很清楚

     * Cache readers (PC-checked by collecting_in_critical())
     * objc_msgSend*
     * cache_getImp
     *
     * Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
     * cache_t::copyCacheNolock    (caller must hold the lock)
     * cache_t::eraseNolock        (caller must hold the lock)
     * cache_t::collectNolock      (caller must hold the lock)
     * cache_t::insert             (acquires lock)
     * cache_t::destroy            (acquires lock)
    

    在读缓存的时候,最早会调用objc_msgSend,也就是发送消息时候会进行读取方法。下一个篇章分析objc_msgSend

    相关文章

      网友评论

          本文标题:cache_t底层分析

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