OC底层原理06- cache分析

516 阅读11分钟

前言

上一篇已经找到了类方法在元类的methods()里面 类的成员变量在class_ro_t.ivars里面 这节我们探究 cache_t cache

一.NSObject面试题

Xnip2021-06-24_21-52-26.jpg

+ (BOOL)isKindOfClass:(Class)cls {

    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
        //元类 : cls
        //父元类 : cls
        //根元类 : cls
        //NSObject 类 : cls
        //nil : cls
        
    }
    return NO;
}
例子1. BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; 
NSObject的元类就是(根元类)  和  NSObject类比  = NO
根元类的父类是 NSObject类    和  NSObject类比  = YES
打印 re1 = 1

例子2. BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];
LGPerson元类  和  LGPerson类比  = NO
LGPerson元类的父类就是(根元类)   和  LGPerson类比  = NO
根元类的父类是 NSObject类    和  LGPerson类比  = NO
NSObject类的父类是 nil    和  LGPerson类比  = NO
打印 re3 = 0

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls; //元类 : cls
}
例子3.  BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; 
NSObject的元类就是(根元类)  和  NSObject类比  = NO
打印 re2 = 0

例子4. BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];
LGPerson的元类  和  LGPerson类比  = NO
打印 re4 = 0


- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
        //类 : cls
        //父类: cls
        //NSObject 类 : cls
        //nil : cls
    }
    return NO;
}
例子5. BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
NSObject的类 和  NSObject 类比 = YES
打印 re5 = 1

例子6.  BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; 
LGPersonLGPerson 类比 = YES
打印 re7 = 1



- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls; //类 : cls 
}

例子7. BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
NSObject的类 和  NSObject 类比 = YES
打印 re6 = 1

例子8. BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];
LGPersonLGPerson 类比 = YES
打印 re8 = 1

这个结果是对的 但是分析的真的对吗 好多博客都是这么写的 但是真的是这么执行的吗 我们全局断点 看一下汇编

Xnip2021-06-24_22-26-29.jpg isMemberOfClass是有找到 isKindOfClass 没有找到 但是找到了一个objc)opt_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);
} 
所以当obj是实例对象的时候,obj->getIsa()等同于 [obj class],拿到对象的类。
当obj是对象的时候,obj->getIsa()就是拿类的元类。

二.cache_t数据结构

cache_t的结果如下图

Xnip2021-06-24_23-17-11.jpg

  • _bucketsAndMaybeMask变量uintptr_t8字节isa_t中的bits类似,也是一个指针类型里面存放地址
  • 联合体里有一个结构体和一个结构体指针_originalPreoptCache
  • 结构体中有三个成员变量 _maybeMask_flags_occupied__LP64__指的是Unix和Unix类系统(Linx和macOS)
  • _originalPreoptCache和结构体是互斥的,_originalPreoptCache初始时候的缓存,现在探究类中的缓存,这个变量基本不会用到

我们缓存的属性 方法 都在哪里呢 我们来看一下cache_t里面比较重要的东西

   void incrementOccupied();
   void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
   void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
   void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
   public:
       unsigned capacity() const;
       struct bucket_t *buckets() const;//内存平移去取值
       Class cls() const;
       mask_t occupied() const;
   void insert(SEL sel, IMP imp, id receiver);

cache_t提供了公用的方法去获取值,以及根据不同的架构系统去获取maskbuckets的掩码


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 区分真机和其它,但是变量没变都是 _sel _imp 只不过顺序不一样
  • bucket_t 里面存的是 _sel _imp cache 里面缓存的应该是方法 接下来我们通过LLDB调试一下 验证方法的存储

三.004-cache底层LLDB分析

Xnip2021-06-24_23-41-31.jpg

我们来验证一下sayNB是否缓存 进入LLDB调试

(lldb) p/x LGPerson.class
(Class) $0 = 0x0000000100008a68 LGPerson
(lldb) p (cache_t *) 0x0000000100008a78 //平移16位
(cache_t *) $1 = 0x0000000100008a78
(lldb) p * $1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4314110400
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32840
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001804800000007
      }
    }
  }
}
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001012419c0

(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 45880
    }
  }
}
(lldb) p $4._imp
(explicit_atomic<unsigned long>) $5 = {
  std::__1::atomic<unsigned long> = {
    Value = 45880
  }
}
(lldb) p $2.buckets()[0]
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 45880
    }
  }
}
(lldb) p $6.sel()//获取sel
(SEL) $9 = "sayNB"
(lldb) p $6.imp(nil,[LGPerson class])
(IMP) $10 = 0x0000000100003950 (KCObjcBuild`-[LGPerson sayNB])

四.脱离源码分析cache

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];
        [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);
        }
        
        打印结果
        
        2021-06-25 00:09:57.502636+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say1]
2021-06-25 00:09:57.503182+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say2]
2021-06-25 00:09:57.503218+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say3]
2021-06-25 00:09:57.503248+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say4]
2021-06-25 00:09:57.503267+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say1]
2021-06-25 00:09:57.503284+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : -[LGPerson say2]
2021-06-25 00:09:57.503307+0800 003-cache_t脱离源码环境分析[36002:5634385] LGPerson say : +[LGPerson sayHappy]
2021-06-25 00:09:57.503325+0800 003-cache_t脱离源码环境分析[36002:5634385] 4 - 7
2021-06-25 00:09:57.503394+0800 003-cache_t脱离源码环境分析[36002:5634385] say4 - 0xb850f
2021-06-25 00:09:57.503535+0800 003-cache_t脱离源码环境分析[36002:5634385] say1 - 0xb9c0f
2021-06-25 00:09:57.503580+0800 003-cache_t脱离源码环境分析[36002:5634385] say3 - 0xb820f
2021-06-25 00:09:57.503915+0800 003-cache_t脱离源码环境分析[36002:5634385] (null) - 0x0f
2021-06-25 00:09:57.503951+0800 003-cache_t脱离源码环境分析[36002:5634385] (null) - 0x0f
2021-06-25 00:09:57.504006+0800 003-cache_t脱离源码环境分析[36002:5634385] say2 - 0xb9f0f
2021-06-25 00:09:57.504064+0800 003-cache_t脱离源码环境分析[36002:5634385] (null) - 0x0f

底层原理 a: 1-3 -> 1-7??? 内存扩容

b: (null) - 0x0 方法去哪???

c: 2 - 7 + say4 - 0xb850 + 没有类方法???

d: NSObject 父类???

五.cache_t源码探究

cache_t里面有需要用到下面方法进行插入: void insert(SEL sel, IMP imp, id receiver); 怎么插入的呢 我们分析一下insert方法

计算当前所占容量

Xnip2021-07-03_20-25-52.jpg

  • occupied()获取当前所占的容量,其实就是告诉你缓存中有几个bucket

  • newOccupied = occupied() + 1,表示你是第几个进来缓存的

  • oldCapacity 目的是为了重新扩容的时候释放旧的内存

  • 只有第一次缓存方法的时,才会去开辟容量默认开辟容量是 capacity = INIT_CACHE_SIZEcapacity = 4 就是4个bucket的内存大小

  • reallocate(oldCapacity, capacity, /* freeOld */false)开辟内存,freeOld变量控制是否释放旧的内存

开辟容量

Xnip2021-07-03_20-30-30.jpg reallocate 方法主要做三件事

  • allocateBuckets开辟内存
  • setBucketsAndMask设置maskbuckets的值
  • collect_free是否释放旧的内存,由freeOld控制

探究一下allocateBuckets这个方法

Xnip2021-07-03_20-31-56.jpg

allocateBuckets 方法主要做两件事

  • calloc(bytesForCapacity(newCapacity), 1)开辟newCapacity * bucket_t 大小的内存
  • end->set将开辟内存的最后一个位置存入sel = 1imp = 第一个buket位置的地址

setBucketsAndMask方法探究

Xnip2021-07-03_20-37-56.jpg

setBucketsAndMask主要根据不同的架构系统向_bucketsAndMaybeMask_maybeMask写入数据

collect_free方法探究

Xnip2021-07-03_20-40-49.jpg

collect_free主要是清空数据,回收内存

setBucketsAndMask方法探究

容量小于3/4

Xnip2021-07-03_21-06-15.jpg

容量存满

Xnip2021-07-03_21-07-09.jpg

容量超过3/4

Xnip2021-07-03_21-07-43.jpg

缓存方法

Xnip2021-07-03_21-09-24.jpg

  • 首先拿到bucket()指向开辟这块内存首地址,也就是第一个bucket的地址,bucket()既不是数组也不是链表,只是一块连续的内存
  • hash函数根据缓存selmask,计算出hash下标。为什么需要mask呢?mask的实际作用是告诉系统你只能存前capacity - 1中的位置,比如capacity = 4时,缓存的方法只能存前面3个空位
  • 开始缓存,当前的位置没有数据,就缓存该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接return。如果存在hash冲突,下标一样,sel不一样,此时会进行再次hash,冲突解决继续缓存

cache_next

Xnip2021-07-03_21-12-20.jpg

cache_hash

Xnip2021-07-03_21-14-01.jpg

cache_hash主要是生成hash下标cache_next主要是解决hash冲突

缓存写入方法set

Xnip2021-07-03_21-17-54.jpg

setselimp写入bucket,开始缓存方法

incrementOccupied

Xnip2021-07-03_21-19-15.jpg

_occupied自动加1_occupied表示内存中已经存储缓存方法的的个数

总结

  • _bucketsAndMaybeMask(buckets()的首地址)存储bucketsmask(真机),macOS或者模拟器存储buckets
  • _maybeMask是指掩码数据,用于在哈希算法或者哈希冲突算法中哈希下标 _maybeMask = capacity -1
  • _occupied会随着缓存的个数增加,扩容是_occupied = 0
  • 数据丢失是因为扩容的时候旧的内存回收了数据全部清除
  • cache存储bucket的位置乱序,因为位置是hash根据你的selmask生成所以不固定

补充

lldb调试出现7lldb调试中,调用一个实例方法,但是_maybeMask = 7。在代码模拟转换的方式中同样调用一个方法_maybeMask = 3。在lldb为什么显示7?首先在源码中打印出 selimp源码如下

Xnip2021-07-04_00-35-59.jpglldb调用实例方法,查看lldb的信息

Xnip2021-07-04_00-36-24.jpg

  • 在调用say1方法,此时还没有缓存该方法,但是之前已经开辟了一次内存,里面3bucket有值
  • 缓存的方法有NSObjectrespondsToSelector方法,NSObjectclass方法,还有一个未知的方法
  • 最后一个未知的方法 sel=0x1,即sel = 1这个熟悉不,在上面探究allocateBuckets方法的时,有一行代码重点探究过end->set<NotAtomic, Raw>``(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil)。最后一个bucket默认存放了sel=1imp = 开辟内存的首地址也就是buckets()的地址。注意此时set的方法的最后一个参数cls传的是nil相当于没有进行对imp编码

验证 sel=0x1imp是否是buckets()的地址,在源码中把buckets()的地址打印出来代码如下

Xnip2021-07-04_00-20-35.jpg

lldb分析 sel=0x1imp的地址和buckets()的地址是一样的 sel=0x1imp为什么要和class进行异或运算。后面有分析和探究

总结

  • lldb调试_maybeMask = 7原因:say1调用时,已经缓存了2个方法respondsToSelectorclasssel=0x1的这个方法,开辟内存的时候就存储了默认的。所以当调用say1时正好是第3个缓存,超过当前容量的3/4,所以去扩容,把respondsToSelectorclass方法缓存清除。把say1放入缓存中。所以lldb呈现给我们的就是 _maybeMask = 7occupied = 1
  • 调用respondsToSelectorclass原因:猜测lldb的环境和运行的环境不一样。第一次[LGPerson alloc]是会初始化NSObject[NSObject alloc],所以respondsToSelectorclass会缓存到NSObject类中。lldb环境不会初始化NSObject[NSObject alloc],调用实例方法,它会到NSObject中去找,然后缓存到LGPerson类中

imp编码解码

bucket中的的imp地址,存储的是经过编码以后强转成uintptr_t类型数据,解码是会还原成原来的imp

imp编码

Xnip2021-07-04_00-23-55.jpg

Xnip2021-07-04_00-24-18.jpg

  • b[i].set<Atomic, Encoded>(b, sel, imp, cls()) 缓存selimpset方法内会调用encodeImpencodeImp方法会对imp进行编码(uintptr_t)newImp ^ (uintptr_t)cls即异或运算
  • bucket里面的imp是否进行编解码,除了外部变量控制以外,主要是看bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls))cls参数是否为nil。cls有值imp进行编码,cls没有值imp相当于没编码。所以缓存开辟内存的最后一个bucket调用set方法时cls = nil编码以后得到的还是原来的imp相当于没编码

imp解码 Xnip2021-07-03_23-17-08.jpg

imp解码的方式即异或运算和imp编码的异或运算是一样的 上面lldb调试出现7中打印信息调用imp(nil, cls())imp(nil, cls())对最后一个bucketimp进行一次异或运算,所以想要恢复imp的原来的地址,需要手动进行一次异或运算

异或运算

异或运算:参与运算的两个值,如果两个相应位相同,则结果为0,否则为1

a = 2 0000 0010

b = 7 0000 0111

c = a ^ b 0000 0010 ^ 0000 0111 = 0000 0101 = 5

a = c ^ b 0000 0101 ^ 0000 0111 = 0000 0010 = 2