美文网首页iOS开发技巧
OC底层原理06-cache_t探究

OC底层原理06-cache_t探究

作者: 夏天的枫_ | 来源:发表于2020-09-20 00:36 被阅读0次

    iOS--OC底层原理文章汇总

    前言

    本文主要探索cache_t * cache结构内容,分析它在类的结构中扮演了什么样的角色。
    通过前面OC底层原理04的介绍,我们对类的结构有了一个清晰的了解,总结如下

    Class结构

    OC底层原理03— isa探究中,通过objc_object源码 对isa进行了探究,OC底层原理04对bits做了探索。现在来探究cachecache字面意思--缓存很好理解,那它缓存了什么内容呢?已经它缓存的结构又是怎么样呢?
    缓存: 缓存了增删改查操作 ,目的为了操作安全。每调用一次方法,就缓存下来。

    从源码出发

    我们来看看cache_t源码

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; 
    // explicit_atomic:显示原子性,为了在缓存增删改查操作的操作安全,提高线程的安全性。
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets; //arm64与mac环境不一样,将二者结合在一起使用,目的是优化,提高效率。
        mask_t _mask_unused;
        // How much the mask is shifted by.
        static constexpr uintptr_t maskShift = 48;
        // Additional bits after the mask which must be zero. msgSend
        // takes advantage of these additional bits to construct the value
        // `mask << 4` from `_maskAndBuckets` in a single instruction.
        static constexpr uintptr_t maskZeroBits = 4;
        // The largest mask value we can store.
        static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
        // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
        static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; // 类似isa的面具
        // 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
        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; // 
    

    结构体CACHE_MASK_STORAGE的三种情况:
    CACHE_MASK_STORAGE_OUTLINED: 模拟器 \ macOS 环境
    CACHE_MASK_STORAGE_HIGH_16: 真机环境
    CACHE_MASK_STORAGE_LOW_4: 不是64位的真机环境
    其他属性:
    bucke_t: 装着imp , sel
    mask:类似isa中的mask(面具)
    _flags: 位置标记
    _occupied: 占位,缓存占用量

    通过图我们再来宏观上感受下cache_t的结构

    • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
      cache_t的结构
    • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
      maskAndBucket时cache_t结构

    LLDB调试之

    objc-781基础上定义一个类,编写一些方法:

    @interface LGPerson : NSObject
    @property (nonatomic, copy) NSString *lgName;
    @property (nonatomic, strong) NSString *nickName;
    
    - (void)sayHello;
    
    - (void)sayCode;
    
    - (void)sayMaster;
    
    - (void)sayNB;
    
    + (void)sayHappy;
    
    @end
    // LGPerson.m
    #import "LGPerson.h"
    
    @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
    

    在main.m中调用它,在[p sayHello];打下断点,在调试窗口进行调试

     LGPerson *p  = [LGPerson alloc];
     Class pClass = [LGPerson class];
     [p sayHello];
     [p sayCode];
     [p sayMaster];
    

    按照之前宏观下分析的结构,调试结果如下:

    (lldb) p/x pClass // 打印类的指针地址
    (Class) $0 = 0x0000000100002298 LGPerson
    (lldb) p (cache_t *)0x00000001000022a8 // 采用获取bits类似的方式获取cacche,这里指针移动16位,即加上0x10.
    (cache_t *) $1 = 0x00000001000022a8
    (lldb) p *$1 // 打印出cache_t的结构
    (cache_t) $2 = {
      _buckets = {
        std::__1::atomic<bucket_t *> = 0x000000010032e430 {
          _sel = {
            std::__1::atomic<objc_selector *> = (null)
          }
          _imp = {
            std::__1::atomic<unsigned long> = 0
          }
        }
      }
      _mask = {
        std::__1::atomic<unsigned int> = 0
      }
      _flags = 32804
      _occupied = 0
    }
    2020-09-20 00:03:34.346793+0800 KCObjc[8619:340927] LGPerson say : -[LGPerson sayHello]
    (lldb) p *$1
    (cache_t) $3 = {
      _buckets = {
        std::__1::atomic<bucket_t *> = 0x000000010072c870 {
          _sel = {
            std::__1::atomic<objc_selector *> = ""
          }
          _imp = {
            std::__1::atomic<unsigned long> = 11928
          }
        }
      }
      _mask = {
        std::__1::atomic<unsigned int> = 3
      }
      _flags = 32804
      _occupied = 1
    }
    

    这里做了一个操作,当第一次p *$1时断点停留在 [p sayHello];,然后断点走到下一步,代码执行了 -[LGPerson sayHello]操作,再次执行p *$1_sel、_imp、_flags 、_occupied的值发生了变化。初步验证每调用一次方法,就将其缓存。
    我们继续调试

    (lldb) p $3.buckets() // 打印buckets数组
    (bucket_t *) $4 = 0x000000010072c870
    (lldb) p *$4
    (bucket_t) $5 = {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11928
      }
    }
    (lldb) p $5.sel() // sel()调用方式是查找cache_t下的定义方法确定的
    (SEL) $6 = "sayHello"
    (lldb) p $5.imp(pClass)  // 同样查看imp是否有相关实现方法:imp(Class cls)
    (IMP) $7 = 0x0000000100000c00 (KCObjc`-[LGPerson sayHello]) // 打印出带具体方法实现的函数指针 imp
    
    

    我们再通过MachOView软件查找可执行文件中的方法以验证正确性。软件具体使用可参考这里

    MachOView验证
    结果显示一致。

    脱离源码环境分析

    通过源码我们能初探到了 cache_t中的结构,现在我们再脱离源码环境基础上再来分析下。
    新建一个mac工程,创建一个Book类,属性及方法具体如下:

    @interface Book : NSObject
    @property (nonatomic,copy) NSString * bookName;
    @property (nonatomic,copy) NSString * version;
    
    - (void)read1;
    - (void)read2;
    - (void)read3;
    - (void)read4;
    @end
    // Book.m
    #import "Book.h"
    @implementation Book
    - (void)read1{
        NSLog(@"read book : %s",__func__);
    }
    - (void)read2{
        NSLog(@"read book : %s",__func__);
    }
    - (void)read3{
        NSLog(@"read book : %s",__func__);
    }
    - (void)read4{
        NSLog(@"read book : %s",__func__);
    }
    @end
    

    在main.m中从源码工程中组建一份与cache_t结构相似的代码,去除一些非本研究内容:

    
    #import <Foundation/Foundation.h>
    #import "Book.h"
    #import <objc/runtime.h>
    typedef uint32_t mask_t;
    
    struct my_buckt_t { // bucket中存储的就是sel,imp,所以保留两个即可
        SEL _sel;
        IMP _imp;
    };
    struct my_cache_t { // 只保留cache_t中重要的研究对象
        struct my_buckt_t * _buckets;
        mask_t _mask;
        uint16_t _flags;
        uint16_t _occupied;
    };
    struct my_class_data_bits_t{  // 简化bits
        uintptr_t bits;
    };
    struct my_objc_class { // 取消继承自objc_object
        Class ISA; // 由于未继承了,所以得打开isa注释
        Class superclass;
        struct my_cache_t cache;             // formerly cache pointer and vtable
        struct my_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    };
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            Book * book = [Book alloc];
            Class bClass = [Book class];
    //        book.bookName = @"Object-C";
    //        book.version = @"1.0.0";
            
            [book read1];
    //        [book read2];
    //        [book read3];
            struct my_objc_class * my_bClass = (__bridge struct my_objc_class *)(bClass);
            NSLog(@"occupied:%hu - mask:%u",my_bClass->cache._occupied, my_bClass->cache._mask);
            for (mask_t i = 0; i < my_bClass->cache._mask; i ++) {
                // 打印获取的bucket
                struct my_buckt_t  bucket = my_bClass->cache._buckets[I];
                NSLog(@"sel:%@ - imp:%p",NSStringFromSelector(bucket._sel),bucket._imp);
            }
        }
        return 0;
    }
    

    执行之后会得到下面结果


    其中_occupied = 1, mask = 3,在打印出的buckets数组中,只有第一个有值。当我们打开read2,read3的注释,结果就会是这样:

    随着方法调用的增加,mask的值发生了变化。而sel、imp并没有随着调用方法增加,而增加值,依然是有很多的控制。那么occupied,mask是什么?底层是怎么做的?接下来我们继续本文重点内容,cache底层原理。

    cache底层原理

    我们切换至781源码工程,再找到cache_t结构定义文件,搜索occupied,我们找到这样的代码,找到了增加occupied的函数:


    搜索调用改方法的地方找到了
    // 调用之处,自身加1
    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    //---
    ALWAYS_INLINE
    void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
    {
    #if CONFIG_USE_CACHE_LOCK
        cacheUpdateLock.assertLocked();
    #else
        runtimeLock.assertLocked();
    #endif
        ASSERT(sel != 0 && cls->isInitialized());
        // 当缓存少于3/4时,按原样使用
        mask_t newOccupied = occupied() + 1;
        unsigned oldCapacity = capacity(), capacity = oldCapacity;
        if (slowpath(isConstantEmptyCache())) {
            // Cache is read-only. Replace it. 
            if (!capacity) capacity = INIT_CACHE_SIZE;
            reallocate(oldCapacity, capacity, /* freeOld */false);
        }
        else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 4
            if (capacity > MAX_CACHE_SIZE) {
                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;
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the
        // minimum size is 4 and we resized at 3/4 full.
        do {
            if (fastpath(b[i].sel() == 0)) {
                incrementOccupied();
                b[i].set<Atomic, Encoded>(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::bad_cache(receiver, (SEL)sel, cls);
    }
    
    1.计算缓存占用量

    mask_t newOccupied = occupied() + 1;这里是新建一个newOccupied,这一步是为了计算缓存占用量。这里有个需要注意的点,occupied的值在[p init]时,occupied = 1;当给属性赋值时会调用set方法,也会是得occupied的值增加。在脱离源码分析的工程中,我们未调用init、属性,这就解释了我们打印的occupied的为1。

    2.开辟空间
    • 1)默认开辟4个单位的缓存空间
    • 2)缓存占用量少于 3/4 时,不做操作
    • 3)其他情况,在原缓存空间基础上扩容2倍。
    if (slowpath(isConstantEmptyCache())) { // 如果未空,执行初始化操作
            // Cache is read-only. Replace it. 
            if (!capacity) capacity = INIT_CACHE_SIZE; // INIT_CACHE_SIZE = 1 << 2 = 0100 = 4
            reallocate(oldCapacity, capacity, /* freeOld */false); // 初始化 开辟空间
     } else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
            // Cache is less than 3/4 full. Use it as-is.
     }else {
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍  2 * 4 
            if (capacity > MAX_CACHE_SIZE) {
                capacity = MAX_CACHE_SIZE;
            }
            reallocate(oldCapacity, capacity, true);  // 内存库容完毕
        }
    

    reallocate

    3.缓存bucket - sel & mask
        bucket_t *b = buckets();
        mask_t m = capacity - 1;
        mask_t begin = cache_hash(sel, m);
        mask_t i = begin;
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the
        // minimum size is 4 and we resized at 3/4 full.
        do {
            if (fastpath(b[i].sel() == 0)) {
                incrementOccupied();
                b[i].set<Atomic, Encoded>(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::bad_cache(receiver, (SEL)sel, cls);
    

    mask_t m = capacity - 1;就是之前脱离源码环境分析的sel = 3,mask = 7,未扩容和扩容的两个情况。
    mask_t begin = cache_hash(sel, m); 这个是用到哈希算法。如果在之前基础上进行了扩容,那么利用哈希算法就得找到之前存储的位置进行重排存储。计算得到sel下标,
    1).如果第一次进入存储,那么sel的下标是从0开始存储,此时occupied从0开始加1;
    2).当第二次进入的存储过程,sel下标如果和即将插入存储的sel相同,则进行原样存储;
    3).当再次新存储的sel在遍历bucket存储空间,已经存储,那么就要cache_next,算法重排,再次计算一个新的下标进行存储。
    所以在bucket扩容后,会重新生成,所以会有看起来原来位置的sel为空,其实只是梳理了空间,换了个存储位置,而这个位置是乱序查找插入的,非遍历形式的好处是存储和取出的效率更高。
    cache_hash:对存储进行哈希

    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask; // mask = capacity -1 ,sel & mask
    }
    

    cache_next哈希重排

    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;  // 1 & 7 = 1, 2 & 7 = 2, 1则就可以作为返回值以供查找下标
    }
    

    总结

    通过一顿操作探究,我们得到了以下的信息:
    1.在cache中,occupied是一个缓存占用量,用来判断是否需要进行扩容,大于容量的3/4时就必须要扩容;
    2.mask字面意思数面具,是哈希存储过程中的下标。它等于 capacity - 1;,如果扩容之后是8 ,那么在利用哈希存储的时候,最高就到7,不然就要越界了;
    3.在方法调用(包括init)、属性设置过程中,都会引起occupied的增加;
    4.扩容后的cache,在存储bucket时,是利用哈希算法存储的,哈希算法存储会比遍历效率更高。

    相关文章

      网友评论

        本文标题:OC底层原理06-cache_t探究

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