前言
上一篇已经找到了类方法在元类的methods()里面 类的成员变量在class_ro_t.ivars里面 这节我们探究 cache_t cache
一.NSObject面试题
+ (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]];
LGPerson 和 LGPerson 类比 = 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]];
LGPerson 和 LGPerson 类比 = YES
打印 re8 = 1
这个结果是对的 但是分析的真的对吗 好多博客都是这么写的 但是真的是这么执行的吗 我们全局断点 看一下汇编
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的结果如下图
_bucketsAndMaybeMask变量uintptr_t占8字节和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提供了公用的方法去获取值,以及根据不同的架构系统去获取mask和buckets的掩码
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分析
我们来验证一下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方法
计算当前所占容量
-
occupied()获取当前所占的容量,其实就是告诉你缓存中有几个bucket了 -
newOccupied=occupied()+ 1,表示你是第几个进来缓存的 -
oldCapacity目的是为了重新扩容的时候释放旧的内存 -
只有第一次缓存方法的时,才会去开辟容量默认开辟容量是
capacity = INIT_CACHE_SIZE即capacity = 4就是4个bucket的内存大小 -
reallocate(oldCapacity, capacity, /* freeOld */false)开辟内存,freeOld变量控制是否释放旧的内存
开辟容量
reallocate 方法主要做三件事
allocateBuckets开辟内存setBucketsAndMask设置mask和buckets的值collect_free是否释放旧的内存,由freeOld控制
探究一下allocateBuckets这个方法
allocateBuckets 方法主要做两件事
calloc(bytesForCapacity(newCapacity), 1)开辟newCapacity * bucket_t大小的内存end->set将开辟内存的最后一个位置存入sel=1,imp=第一个buket位置的地址
setBucketsAndMask方法探究
setBucketsAndMask主要根据不同的架构系统向_bucketsAndMaybeMask 和 _maybeMask写入数据
collect_free方法探究
collect_free主要是清空数据,回收内存
setBucketsAndMask方法探究
容量小于3/4
容量存满
容量超过3/4
缓存方法
- 首先拿到
bucket()指向开辟这块内存首地址,也就是第一个bucket的地址,bucket()既不是数组也不是链表,只是一块连续的内存 hash函数根据缓存sel和mask,计算出hash下标。为什么需要mask呢?mask的实际作用是告诉系统你只能存前capacity - 1中的位置,比如capacity = 4时,缓存的方法只能存前面3个空位- 开始缓存,当前的位置没有数据,就缓存该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接
return。如果存在hash冲突,下标一样,sel不一样,此时会进行再次hash,冲突解决继续缓存
cache_next
cache_hash
cache_hash主要是生成hash下标,cache_next主要是解决hash冲突
缓存写入方法set
set把sel和imp写入bucket,开始缓存方法
incrementOccupied
_occupied自动加1,_occupied表示内存中已经存储缓存方法的的个数
总结
_bucketsAndMaybeMask(buckets()的首地址)存储buckets和mask(真机),macOS或者模拟器存储buckets_maybeMask是指掩码数据,用于在哈希算法或者哈希冲突算法中哈希下标_maybeMask=capacity -1_occupied会随着缓存的个数增加,扩容是_occupied = 0- 数据丢失是因为扩容的时候旧的内存回收了数据全部清除
cache存储bucket的位置乱序,因为位置是hash根据你的sel和mask生成所以不固定
补充
lldb调试出现7
在lldb调试中,调用一个实例方法,但是_maybeMask = 7。在代码模拟转换的方式中同样调用一个方法_maybeMask = 3。在lldb为什么显示7?首先在源码中打印出 sel和imp源码如下
在
lldb调用实例方法,查看lldb的信息
- 在调用
say1方法,此时还没有缓存该方法,但是之前已经开辟了一次内存,里面3个bucket有值 - 缓存的方法有
NSObject的respondsToSelector方法,NSObject的class方法,还有一个未知的方法 - 最后一个未知的方法
sel=0x1,即sel = 1这个熟悉不,在上面探究allocateBuckets方法的时,有一行代码重点探究过end->set<NotAtomic, Raw>``(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil)。最后一个bucket默认存放了sel=1和imp= 开辟内存的首地址也就是buckets()的地址。注意此时set的方法的最后一个参数cls传的是nil相当于没有进行对imp编码
验证 sel=0x1的imp是否是buckets()的地址,在源码中把buckets()的地址打印出来代码如下
lldb分析 sel=0x1的imp的地址和buckets()的地址是一样的
sel=0x1的imp为什么要和class进行异或运算。后面有分析和探究
总结
lldb调试_maybeMask = 7原因:say1调用时,已经缓存了2个方法respondsToSelector和class。sel=0x1的这个方法,开辟内存的时候就存储了默认的。所以当调用say1时正好是第3个缓存,超过当前容量的3/4,所以去扩容,把respondsToSelector和class方法缓存清除。把say1放入缓存中。所以lldb呈现给我们的就是_maybeMask = 7,occupied = 1- 调用
respondsToSelector和class原因:猜测lldb的环境和运行的环境不一样。第一次[LGPerson alloc]是会初始化NSObject即[NSObject alloc],所以respondsToSelector和class会缓存到NSObject类中。lldb环境不会初始化NSObject即[NSObject alloc],调用实例方法,它会到NSObject中去找,然后缓存到LGPerson类中
imp编码解码
bucket中的的imp地址,存储的是经过编码以后强转成uintptr_t类型数据,解码是会还原成原来的imp
imp编码
b[i].set<Atomic, Encoded>(b, sel, imp, cls())缓存sel,imp。set方法内会调用encodeImp。encodeImp方法会对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解码
imp解码的方式即异或运算和imp编码的异或运算是一样的
上面lldb调试出现7中打印信息调用imp(nil, cls()),imp(nil, cls())对最后一个bucket的imp进行一次异或运算,所以想要恢复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