cache分析

作者: 浅墨入画 | 来源:发表于2021-07-22 23:22 被阅读0次
    面试题iskindOfClass & isMemberOfClass的理解

    下面是关于iskindOfClass & isMemberOfClass的代码,分析最终结果

    //-----使用 iskindOfClass & isMemberOfClass 类方法
    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);
    
    //------iskindOfClass & isMemberOfClass 实例方法
    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);
    
    // 打印结果
    re1 : 1
    re2 : 0
    re3 : 0
    re4 : 0
    re5 : 1
    re6 : 1
    re7 : 1
    re8 : 1
    
    源码解析
    • isKindOfClass源码解析(实例方法 & 类方法)
    // + isKindOfClass:第一次比较是 获取类的元类 与 传入类对比,再次之后的对比是获取上次结果的父类 与 传入 类进行对比
    + (BOOL)isKindOfClass:(Class)cls {
        // 获取类的元类 vs 传入类
        // 根元类 vs 传入类
        // 根类 vs 传入类
        for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    // - isKindOfClass:第一次是获取对象类 与 传入类对比,如果不相等,后续对比是继续获取上次 类的父类 与传入类进行对比
    - (BOOL)isKindOfClass:(Class)cls {
        // 获取对象的类 vs 传入的类 
        // 父类 vs 传入的类
        // 根类 vs 传入的类
        // nil vs 传入的类
        for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    • isMemberOfClass源码解析(实例方法 & 类方法)
    //-----类方法
    //+ isMemberOfClass : 获取类的元类,与 传入类对比
    + (BOOL)isMemberOfClass:(Class)cls {
        return self->ISA() == cls;
    }
    //-----实例方法
    //- isMemberOfClass : 获取对象的类,与 传入类对比
    - (BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }
    
    源码分析总结
    • isKindOfClass
    • 类方法:元类(isa) --> 根元类(父类) --> 根类(父类) --> nil(父类) 与 传入类的对比
    • 实例方法:对象的类 --> 父类 --> 根类 --> nil与 传入类的对比
    • isMemberOfClass

    类方法: 类的元类 与 传入类 对比
    实例方法:对象的类 与 传入类 对比

    然后通过断点调试查看汇编,isMemberOfClass的类方法和实例方法的流程是正常的,会走到上面分析的源码,而isKindOfClass根本不会走到上面分析的源码中(!!!注意这里,这是一个坑点),其类方法和实例方法都是走到objc_opt_isKindOfClass方法源码中

    objc_opt_isKindOfClass方法源码如下
    // Calls [obj isKindOfClass]
    BOOL
    objc_opt_isKindOfClass(id obj, Class otherClass)
    {
    #if __OBJC2__
        if (slowpath(!obj)) return NO;
        //获取isa,
        //如果obj 是对象,则isa是类,
        //如果obj是类,则isa是元类
        Class cls = obj->getIsa(); 
        if (fastpath(!cls->hasCustomCore())) {
            // 如果obj 是对象,则在类的继承链进行对比,
            // 如果obj是类,则在元类的isa中进行对比
            for (Class tcls = cls; tcls; tcls = tcls->superclass) { 
                if (tcls == otherClass) return YES;
            }
            return NO;
        }
    #endif
        return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
    }
    

    主要是因为在llvm中编译时对其进行了优化处理
    调用objc_opt_isKindOfClass实际走的逻辑如下图

    image.png

    cache数据结构

    cache_t是什么?cache中存储的又是什么?

    打开objc4-818.2源码,创建LGPerson类,添加如下代码。NSLog(@"%@",pClass);添加断点,运行工程进行lldb调试

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *p = [LGPerson alloc];
            Class pClass = [LGPerson class];
            NSLog(@"%@",pClass);
        }
        return 0;
    }
    
    // lldb调试内容
    (lldb) p/x pClass
    (Class) $0 = 0x0000000100008400 LGPerson
    (lldb) p (cache_t *)0x0000000100008410
    (cache_t *) $1 = 0x0000000100008410
    (lldb) p *$1
    (cache_t) $2 = {
      _bucketsAndMaybeMask = {
        std::__1::atomic<unsigned long> = {
          Value = 4298515296
        }
      }
       = {
         = {
          _maybeMask = {
            std::__1::atomic<unsigned int> = {
              Value = 0
            }
          }
          _flags = 32808
          _occupied = 0
        }
        _originalPreoptCache = {
          std::__1::atomic<preopt_cache_t *> = {
            Value = 0x0000802800000000
          }
        }
      }
    }
    
    • 查看cache_t源码如下
    struct cache_t {
    private:
        explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
        union {
            struct {
                explicit_atomic<mask_t>    _maybeMask; // 4
    #if __LP64__
                uint16_t                   _flags;  // 2
    #endif
                uint16_t                   _occupied; // 2
            };
            explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
        };
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        // _bucketsAndMaybeMask is a buckets_t pointer
        // _maybeMask is the buckets mask
    
        static constexpr uintptr_t bucketsMask = ~0ul;
        static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        static constexpr uintptr_t maskShift = 48;
        static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
        static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
        
        static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
    #if CONFIG_USE_PREOPT_CACHES
        static constexpr uintptr_t preoptBucketsMarker = 1ul;
        static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
    #endif
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
        // _maybeMask is unused, the mask is stored in the top 16 bits.
    ......
    
    • 查看bucket_t的源码,分为两个版本真机 和 非真机,不同的区别在于selimp的顺序不一致
    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中缓存的是sel-imp,在类的方法调用过程中,已知过程是通过SEL(方法编号)在内存中查找IMP(方法指针),为了使方法响应更加快速效率更高,不需要每一次都去内存中把方法遍历一遍,cache_t结构体出现了。cache_t将调用过的方法的SEL和IMP以及receiver以bucket_t结构体方式存储在当前类结构中,以便后续方法的查找。

    • _bucketsAndMaybeMask:存放数据的bit信息,类似于isa不同bit位存放的数据是什么,当前存放的是buckets和maybeMask
    • _maybeMask:当前的缓存区count,第一次开辟是3
    • _occupied:当前cache的可存储的buckets数量,默认是0
    • incrementOccupied():执行_occupied++,_occupied默认是0,每次有方法的插入都会被执行,本质上就是占位+1
    cache_t结构流程
    cache_t结构流程

    cache底层lldb分析

    通过lldb验证cache_t方法的存储内容

    继续上面的lldb调试

    (lldb) p $2._bucketsAndMaybeMask
    (explicit_atomic<unsigned long>) $3 = {
      std::__1::atomic<unsigned long> = {
        Value = 4298515296
      }
    }
    (lldb) p $2._maybeMask
    (explicit_atomic<unsigned int>) $4 = {
      std::__1::atomic<unsigned int> = {
        Value = 0
      }
    }
    (lldb) p $2._originalPreoptCache
    (explicit_atomic<preopt_cache_t *>) $5 = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
    // 其中$3 $4 $5分别打印Value,都会报错,取不到值
    (lldb) p $3.Value
    (lldb) p $3->Value
    

    $3 $4 $5分别打印Value,都会报错取不到值,这个时候我们就该考虑从源码中找获取值的方法,我们找到了buckets(),那么就继续进行lldb调试

    (lldb) p $2.buckets()
    (bucket_t *) $6 = 0x0000000100362360
    (lldb) p *$6 
    (bucket_t) $7 = {
      _sel = {
        std::__1::atomic<objc_selector *> = (null) {
          Value = (null)
        }
      }
      _imp = {
        std::__1::atomic<unsigned long> = {
          Value = 0
        }
      }
    }
    

    上面调试$7_sel对应的Value值为null,这是为什么呢?因为这个时候还没有调用方法,没有内容可缓存。下面调用方法后继续调试

    (lldb) p [p saySomething]
    2021-07-21 23:05:12.808682+0800 KCObjcBuild[46873:5018317] -[LGPerson saySomething]
    (lldb) p/x pClass
    (Class) $8 = 0x0000000100008400 LGPerson
    (lldb) p (cache_t *)0x0000000100008410
    (cache_t *) $9 = 0x0000000100008410
    (lldb) p *$9
    (cache_t) $10 = {
      _bucketsAndMaybeMask = {
        std::__1::atomic<unsigned long> = {
          Value = 4315035808
        }
      }
       = {
         = {
          _maybeMask = {
            std::__1::atomic<unsigned int> = {
              Value = 7
            }
          }
          _flags = 32808
          _occupied = 1
        }
        _originalPreoptCache = {
          std::__1::atomic<preopt_cache_t *> = {
            Value = 0x0001802800000007
          }
        }
      }
    }
    (lldb) p $10.buckets()
    (bucket_t *) $11 = 0x00000001013238a0
    (lldb) p *$11 
    (bucket_t) $12 = {
      _sel = {
        std::__1::atomic<objc_selector *> = (null) {
          Value = (null)
        }
      }
      _imp = {
        std::__1::atomic<unsigned long> = {
          Value = 0
        }
      }
    }
    

    上面调用了saySomething方法,再打印$12_sel对应的值还是null,这该如何解决呢?

    (lldb) p $10.buckets()[1] 
    (bucket_t) $13 = {
      _sel = {
        std::__1::atomic<objc_selector *> = "" {
          Value = ""
        }
      }
      _imp = {
        std::__1::atomic<unsigned long> = {
          Value = 47232
        }
      }
    }
    

    上面打印buckets()第一个元素,发现为null,我们尝试打印buckets()的第二个元素发现_imp对应的Value = 47232,为什么呢?接下来我们就去bucket_t中找看有没有相应的方法,结果找到了sel()

    <!-- objc4-818.2源码 -->
    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.
    ......
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    
    <!-- lldb调试信息 -->
    (lldb) p $13.sel()
    (SEL) $14 = "saySomething"
    (lldb) p $13.imp(nil,pClass)
    (IMP) $15 = 0x0000000100003c80 (KCObjcBuild`-[LGPerson saySomething])
    

    总结 上面调试成功验证cache_t中缓存的是sel-imp

    脱离源码分析

    脱离源码环境,就是将所需的源码部分拷贝至项目中,其完整代码如下

    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    
    struct kc_bucket_t {
        SEL _sel;
        IMP _imp;
    };
    struct kc_cache_t {
        struct kc_bucket_t *_bukets; // 8
        mask_t    _maybeMask; // 4
        uint16_t  _flags;  // 2
        uint16_t  _occupied; // 2
    };
    
    struct kc_class_data_bits_t {
        uintptr_t bits;
    };
    
    // cache class
    struct kc_objc_class {
        Class isa;
        Class superclass;
        struct kc_cache_t cache;             // formerly cache pointer and vtable
        struct kc_class_data_bits_t bits;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *p  = [LGPerson alloc];
            Class pClass = p.class;  // objc_clas
            [p say1];
    
            struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
            NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
        }
        return 0;
    }
    

    源码中objc_classisa属性是继承自objc_object的,但在我们将其拷贝过来时去掉了objc_class的继承关系,打印的结果会有问题如下图所示

    image.png

    加上isa属性后,其正确的打印结果应该是这样的

    image.png

    再次添加代码如下,打印buckets中的impsel

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *p  = [LGPerson alloc];
            Class pClass = p.class;  // objc_clas
            [p say1];
    
            struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
            NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
    
            for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
                struct kc_bucket_t bucket = kc_class->cache._bukets[i];
                NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
            }
        }
        return 0;
    }
    
    // 打印日志如下
    2021-07-22 19:50:51.582854+0800 003-cache_t脱离源码环境分析[50079:5415756] LGPerson say : -[LGPerson say1]
    2021-07-22 19:50:51.583832+0800 003-cache_t脱离源码环境分析[50079:5415756] 1 - 3
    2021-07-22 19:50:51.584157+0800 003-cache_t脱离源码环境分析[50079:5415756] say1 - 0xb820f
    2021-07-22 19:50:51.584229+0800 003-cache_t脱离源码环境分析[50079:5415756] (null) - 0x0f
    2021-07-22 19:50:51.584281+0800 003-cache_t脱离源码环境分析[50079:5415756] (null) - 0x0f
    Program ended with exit code: 0
    
    针对上面的打印结果,有以下几点疑问
    • _mask是什么?
    • _occupied是什么?
    • 为什么随着方法调用的增多,其打印的occupiedmask会变化?
    • bucket数据为什么会有丢失的情况?

    cache底层原理分析

    • 首先从cache_t中的_mask属性开始分析,找cache_t中引起变化的函数,发现了incrementOccupied()函数
    image.png

    incrementOccupied()函数的具体实现如下

    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    
    • 源码中全局搜索incrementOccupied()函数,发现只在cache_tinsert方法有调用
    • insert方法理解为cache_t的插入,而cache中存储的就是sel-imp,所以cache的原理从insert方法开始分析,以下是cache原理分析的流程图
    image.png
    • 全局搜索insert()方法,发现只有cache_fill方法中的调用符合
    • 全局搜索cache_fill,发现在写入之前还有一步操作,即cache读取,即查找sel-imp
    疑问解答
    • _mask是什么?
      _mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask等于capacity - 1

    • _occupied是什么?
      _occupied表示哈希表中sel-imp的占用大小 (即可以理解为分配的内存中已经存储了sel-imp的的个数),
      init会导致occupied变化
      属性赋值,也会隐式调用,导致occupied变化
      方法调用,导致occupied变化

    • 为什么随着方法调用的增多,其打印的occupiedmask会变化?
      因为在cache初始化时,分配的空间是4个,随着方法调用的增多,当存储的sel-imp个数,即newOccupied + CACHE_END_MARKER(等于1)的和 超过 总容量的3/4,例如有4个时,当occupied等于2时,就需要对cache的内存进行两倍扩容

    • bucket数据为什么会有丢失的情况?
      原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的

    相关文章

      网友评论

        本文标题:cache分析

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