美文网首页
iOS底层原理探究07- 方法缓存cache

iOS底层原理探究07- 方法缓存cache

作者: superFool | 来源:发表于2021-09-24 16:39 被阅读0次

    正题开始之前我们先来个开胃小菜巩固一下之前学习的内容

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
        BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
        BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
    
        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
        BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
        BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    

    这段代码输出会是咋样呢


    image.png

    输出的结果是1 0 0 0 1 1 1 1下面的四个都是1这个应该没什么问题咱们平常开发中经常会对对象做这样的判断,但是上面的四个可能就有点问题了,类的判断不怎么做,那我们怎么理解这个结果呢?当然是看源码了 ,直接上源码

    + (BOOL)isMemberOfClass:(Class)cls {
        return self->ISA() == cls;
    }
    
    - (BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }
    
    + (BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    - (BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    

    通过源码可以看到出

    • 实例方法的- (BOOL)isMemberOfClass:(Class)cls是直接判断实例的类跟cls相不相同,实例方法的- (BOOL)isKindOfClass:(Class)cls是比较实例的类和它的所有父类跟cls是否相同
    • 类方法的+ (BOOL)isMemberOfClass:(Class)cls是直接判断该类的元类跟cls相不相同,类方法的+ (BOOL)isKindOfClass:(Class)cls是比较类的元类和元类的所有父类跟cls是否相同
      这样来看输出的结果就好理解了。但是这里还有一个问题实际运行时是否真的调用的是isMemberOfClassisKindOfClass,下面我们来看下汇编
      image.png
      通过汇编可以看到isMemberOfClass:是正常调用的,但是isKindOfClass:变成了objc_opt_isKindOfClass这个方法的调用,接下来看下这个方法的源码
    // Calls [obj isKindOfClass]
    BOOL
    objc_opt_isKindOfClass(id obj, Class otherClass)
    {
    #if __OBJC2__
        if (slowpath(!obj)) return NO;
        Class cls = obj->getIsa();
        if (fastpath(!cls->hasCustomCore())) {
            for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
                if (tcls == otherClass) return YES;
            }
            return NO;
        }
    #endif
        return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
    }
    

    其实objc_opt_isKindOfClass的核心代码也是判断obj的isa指向的类以及isa指向的类的所有父类是否与otherClass相同。

    好了开胃小菜结束开始正餐

    一、cache的数据结构探索

    通过《iOS底层原理探究05》我们已经了解了类的数据结构

    struct objc_class : objc_object {
        ...省略无关代码
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    
        ...省略无关代码
    }
    

    bits前面的两篇的探索中已经探究过了,今天我们来看下class里的cache的数据结构是怎样的,到底存了些啥?网上很多博客说cache是方法的缓存是不是这样呢?cache的数据结构又是咋样的呢?下面就来探索一下。
    先来看下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 *buckets() const;
    }
    

    主要就是一个联合体的结构下面在控制台打印一下看看实际存储的内容
    cache 前面是ISA、superclass这两个指针类型的成员变量所以拿到类的地址往后平移16个字节(一个是8字节)也就是加上0x10


    image.png

    来尝试输出cache_t里存储的内容

    image.png
    分别输出 _bucketsAndMaybeMask、_maybeMask、_originalPreoptCache均没打印我们想要的内容,前面我们已经有了打印方法列表和属性列表的经验,一般这些数据结构都需要使用源码里提供的api打印,接下来看看cache_t中是否也有相应的api,通过上面的源码可以看到源码中提供了struct bucket_t *buckets() const;方法来获取buckets下面我们先来看下bucket_t的结构再在lldb中打印一下
    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
    ...省略无关代码
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
            uintptr_t imp = _imp.load(memory_order_relaxed);
            if (!imp) return nil;
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
            SEL sel = _sel.load(memory_order_relaxed);
            return (IMP)
                ptrauth_auth_and_resign((const void *)imp,
                                        ptrauth_key_process_dependent_code,
                                        modifierForSEL(base, sel, cls),
                                        ptrauth_key_function_pointer, 0);
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
            return (IMP)(imp ^ (uintptr_t)cls);
    #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
            return (IMP)imp;
    #else
    #error Unknown method cache IMP encoding.
    #endif
        }
    ...省略无关代码
    }
    

    通过源码可以看到bucket_t中存储的是sel和imp,下面在控制台里打印试试


    image.png

    通过buckets()方法我们确实打印出了bucket,但是打印出来的数据Value是null,这是为啥呢?这个问题先留这个这儿,这里我们调用的buckets()既然它带个's'那是不是像数组一样存多个bucket呢,我们来试一下


    image.png
    我们去下标为1的元素,打印出来init方法的sel和imp,由此验证了cache里确实存储着方法的缓存。
    但这里我们也产生了一些问题:
    • buckets的数据结构是咋样的
    • 缓存方法的时候开辟多大的空间,是怎么开辟的
    • 方法是怎么被存到缓存里去的
      我们仿照源码的数据结构自己定义一套结构方便我们继续探索
    struct sp_bucket_t {//对应bucket_t 
        SEL _sel;
        IMP _imp;
    };
    
    struct sp_cache_t {//对应cache_t
        struct sp_bucket_t  *buckets;
        uint32_t            _maybeMask;
        uint16_t            _flags;
        uint16_t            _occupied;
    };
    
    struct sp_class_data_bits_t {//对应class_data_bits_t
        
        uintptr_t bits;
    };
    
    struct sp_objc_class{//对应objc_class
     
        Class ISA;
        Class superclass;
        sp_cache_t cache;
        sp_class_data_bits_t bits;
    
    };
    

    定义相应的数据结构之后main方法中就可以添加相应的代码数据cache存储的内容了

            struct sp_objc_class *sp_class = (__bridge struct sp_objc_class *)(pClass);
            
            for (uint32_t i = 0; i < sp_class->cache._maybeMask; i++) {
                struct sp_bucket_t bucket = sp_class->cache.buckets[I];
                NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
            }
            NSLog(@"%hu - %u ",sp_class->cache._occupied,sp_class->cache._maybeMask);
    
    • for循环打印cache中存储的方法
    • 最后打印出缓存的方法个数和开辟的空间


      image.png
    • 当调用say1、say2两个方法时开辟了三个位置,并缓存了这两个方法,但是0号位置空出来了没有存东西
      在加个方法试试


      image.png
    • 当调用say1、say2、say3时,cache只缓存了say3,cache开辟了7个位置

    lldb调试过程中我们遇到了一些问题

    • 开辟的空间和实际缓存的方法数并不一致
    • 缓存的方法并不是顺序存储的会有空位的情况
    • 当开辟的空间从3变为7的时候之前缓存的方法被清空了只剩了个say3
      带着这些问题我们来看源码

    cache的插入方法

    //入参是 需要缓存的方法的SEL IMP 和 方法接收者
    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;
        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 <= cache_fill_ratio(capacity))) {//已开辟的空间还没有存满可以继续存
            // Cache is less than 3/4 or 7/8 full. Use it as-is. 
        }
    #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 {//已开辟的空间已经存满了 进行双倍扩容
            capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
            if (capacity > MAX_CACHE_SIZE) {
                capacity = MAX_CACHE_SIZE;
            }
            reallocate(oldCapacity, capacity, true);//开辟空间
        }
    
        bucket_t *b = buckets();//取出buckets
        mask_t m = capacity - 1;//计算maybemask
        mask_t begin = cache_hash(sel, m);//使用哈希算法计算插入的位置
        mask_t i = begin;//I表示插入位置
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot.
        do {
            if (fastpath(b[i].sel() == 0)) {//如果插入位置是空的 则插入该方法(避免hash冲突)
                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));//如果i位置没有插入成功 通过cache_next找下一个可以插入的位置
    
        bad_cache(receiver, (SEL)sel);//如果while循环走完都找不到可以插入的位置则缓存失败
    #endif // !DEBUG_TASK_THREADS
    }
    

    理一下缓存插入的大致的流程,过程中调用的方法在后面附上源码

    • newOccupied = occupied() + 1这里计算插入当前方法后缓存的方法总数 newOccupied是插入当前方法后的总数 首次进来的时候occupied()为0 ,newOccupied = 1
    • oldCapacity = capacity()获取之前缓存的空间大小,capacity = oldCapacity初始化新的空间大小等于老的capacity是新的空间大小
    • isConstantEmptyCache()判断缓存是否为空 首次insert的时候缓存是空的,进走这个流程
    • if (!capacity) capacity = INIT_CACHE_SIZE; 首次缓存capacity(需要开辟的空间)赋值为4 (INIT_CACHE_SIZE = 4 后面附上源码)
    • eallocate(oldCapacity, capacity, /* freeOld */false);开辟空间
    • fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))判断当前开辟的空间是否够用
    • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;当前开辟的空间不够用的话进行两倍扩容
    • 下面就是插入缓存的流程了
    • bucket_t *b = buckets();先取出buckets
    • m = capacity - 1;mayBeMask设置成开辟的空间减一(这个后面会再看源码)
    • begin = cache_hash(sel, m);通过哈希算法计算插入的位置,这就是方法不是顺序存储的原因,方法存储位置的下标是通过hash算法计算得到的
    • 进入do-while循环把方法插入缓存fastpath(b[i].sel() == 0)判断当时位置是不是为空,为空则调用incrementOccupied()使_occupied + 1,通过b[i].set方法把方法插入缓存
    • b[i].sel() == sel如果之前已经缓存过该方法直接return
    • i = cache_next(i, m)如果前面的两个判断都为假则查找下一个可以插入的位置
    • bad_cache(receiver, (SEL)sel)如果do-while结束也没有找到可以插入的位置则缓存失败

    下面来看下插入流程中调用的方法的源码实现

    mask_t cache_t::occupied() const
    {
        return _occupied;
    }
    
    • 返回_occupied即已经缓存的方法数
    unsigned cache_t::capacity() const
    {
        return mask() ? mask()+1 : 0; 
    }
    
    mask_t cache_t::mask() const
    {
        return _maybeMask.load(memory_order_relaxed);
    }
    
    • mask()返回_maybeMask即开辟的空间大小
    • capacity()如果_maybeMask有值返回_maybeMask + 1否则返回0,因为_maybeMask是等于开辟的空间减一 所以要加回来
    bool cache_t::isConstantEmptyCache() const
    {
        return
            occupied() == 0  &&
            buckets() == emptyBucketsForCapacity(capacity(), false);
    }
    
    • 判断缓存是否为空
    static inline mask_t cache_fill_ratio(mask_t capacity) {
        return capacity * 3 / 4;
    }
    
    • 这个判断是否存满的算法是 判断有没有达到缓存的3/4,达到了就需要对缓存扩容了
    struct bucket_t *cache_t::buckets() const
    {
        uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
        return (bucket_t *)(addr & bucketsMask);
    }
    
    • buckets是通过 _bucketsAndMaybeMask & bucketsMask得到的
    • static constexpr uintptr_t bucketsMask = ~0ul;bucketsMask等于对0取反就是所有二进制位都是1与上_bucketsAndMaybeMask之后得到的还是_bucketsAndMaybeMask本身
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        uintptr_t value = (uintptr_t)sel;
    #if CONFIG_USE_PREOPT_CACHES
        value ^= value >> 7;
    #endif
        return (mask_t)(value & mask);
    }
    
    • cache_hash算法是通过sel的地址hash得到缓存插入位置的
    • 因为与上了m所以得到的位置不会大于m
    • m是外界传进来的 m = capacity - 1 ,而capacity被初始化成INIT_CACHE_SIZE又通过INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)INIT_CACHE_SIZE_LOG2 = 2得到INIT_CACHE_SIZE = 4m= 3 所以我们在lldb中输出的时候得到1 - 3的打印
      扩容之后因为是双倍扩容capacity变成了8 ,m变成7所以扩容后打印的是1 - 7
    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    
    • 这个方法比较简单就是_occupied自增1
      未完待续

    相关文章

      网友评论

          本文标题:iOS底层原理探究07- 方法缓存cache

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