iOS开发底层之类的底层Cache_t 探究 - 07

Posted iOS_developer_zhong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发底层之类的底层Cache_t 探究 - 07相关的知识,希望对你有一定的参考价值。

文章目录


遗漏知识补充

  1. LLDB调试,发现 对象的 isa 和类的 isa 不一样, 而类的 isa 与元类的一样, 那是因为对象的 isa 中不仅包含了存储类, 还包含了 其他的值,如 引用计数, 是否正在释放,weak 等。 

一. 面试题

1、isKindOfClass 与isMemberOfClass 底层探索

做个测试:

// class_data_bits_t
void lgKindofDemo(void)
    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(@" \\n re1 :%hhd\\n re2 :%hhd\\n re3 :%hhd\\n re4 :%hhd\\n",re1,re2,re3,re4);
   // 打印的结果为: 1 0 0 0 

    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(@" \\n re5 :%hhd\\n re6 :%hhd\\n re7 :%hhd\\n re8 :%hhd\\n",re5,re6,re7,re8);
	// 打印的结果为: 1 1 1 1 

可能会对上面的打印结果懵逼,进入底层看看,就会非常清晰,下面展示下 isMemberOfClassisKindOfClass的源码。
📢:打开汇编,查看真正走的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);

为了加深印象,请看iOS 对实例、类对象、元类、根元类验证

后续需要自己去玩一下!!!

二.Cache_t 底层探索

cache_t 底层结构

cache_t LLDB调试

  1. 通过lldb调试下底层的 cache_t是怎么存储方法的。 注意看注释

    哈希结构,方便存储也方便插入、删除,结合了数组和链表的一些优点。

  2. 接上面的操作继续, 拿到了 $14对象,(cache_t对象),我们现在去看看它的内部,有什么方法是可以直接打印出方法名(SEL)以及实现(IMP).

 //SEL方法
    inline SEL sel() const  return _sel.load(memory_order_relaxed); 
//IMP方法
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const 
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
    

// 在cache_t内部有这两个方法,下面继续用lldb调试
  1. LLDB的操作,接第1步,继续操作。
(lldb) p $14.sel()
(SEL) $15 = "saySomething"
(lldb) p $14.imp(nil, pClass)
(IMP) $16 = 0x0000000100003c20 (KCObjcBuild`-[LGPerson saySomething])

cache_t 脱离源码调试技巧。

  1. 把底层的数据结构,复制出来,自定义一个对象与系统一致,然后通过强转层我们自己定义的对象, 然后通过log日志去调试。

==来自大神的代码: ==

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 
		Person *p  = [Person alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say1];
        [p say2];
//        [p say3];

        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
        // 0 - 8136976 count
        // 1 - 3
        // 1: 源码无法调试
        // 2: LLDB
        // 3: 小规模取样
        
        // 底层原理
        // a: 1-3 -> 1 - 7
        // b: (null) - 0x0 方法去哪???
        // c: 2 - 7 + say4 - 0xb850 + 没有类方法
        // d: NSObject 父类
        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);
        
        NSLog(@"Hello, World!");
    
    return 0;

cache_t 底层深入分析。

  1. 找切入点。 通过插入函数,来分析,到底cache 是怎么工作的。
struct cache_t 
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 
    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
    ;

_maybeMask 的含义为总大小个数, _occupied的含义为当前占用的个数。

用上面的脱机代码来跑个测试,看看代码:

        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];
        [p say2];
        
        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);

//打印结果为 2 - 3 ,就是占用2个,总大小为3 。 

//  举例2 

LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say1];
        [p say2];
        [p say3];

        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
// 打印结果为 4 - 7,就是占用4个大小,总大小为7. 
        
  1. 通过上面的代码,就很奇怪,为什么总大小会自动变,系统是用了什么策略进行扩容的。 接下来来探索下 objc源码。
    下面是cache_t的内部结构源码,找到 insert犯法
  2. 进入方法内部
void cache_t::insert(SEL sel, IMP imp, id receiver)

//省略部分代码
    if (slowpath(isConstantEmptyCache()))   //1. 第一次进入
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4个空间
        reallocate(oldCapacity, capacity, /*  开辟4个大小的空间 */false);
    
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))  // 2. 如果空间没有满,就正常处理
        // 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)  //3. 允许占用100%缓存,并且没有超出分配的大小,就正常使用
        // Allow 100% cache utilization for small buckets. Use it as-is.
    
#endif
    else // 4. 其他的情况就是超出了分配的大小,就进行2倍扩容, 以前是4, 现在扩容后就变成了8位。 
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) 
            capacity = MAX_CACHE_SIZE;
        
        reallocate(oldCapacity, capacity, true);
    

疑问1: 扩容是8个,为啥上面调试打印为7呢,继续走入 reallocate方法。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    //1. 原来是这个地方,会减去一个1,难怪外部调试的时候,扩容8,显示7的原因
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) 
        collect_free(oldBuckets, oldCapacity);
    


遗漏

Cache_t 原理图后续补上,几天没睡好了,去补一觉

以上是关于iOS开发底层之类的底层Cache_t 探究 - 07的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发底层之RuntimeObjc_msgSend探究 - 08

iOS开发底层之RuntimeObjc_msgSend探究 - 08

iOS开发底层之类的底层探究下-06

iOS开发底层之类的底层探究下-06

iOS开发底层之类的底层探究-05

iOS底层开发消息发送与转发流程