cache分析
面试题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
实际走的逻辑如下图
![](https://img.haomeiwen.com/i1212147/6eadab43d6d2a217.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
的源码,分为两个版本真机 和 非真机,不同的区别在于sel
和imp
的顺序不一致
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结构流程
![](https://img.haomeiwen.com/i1212147/093b82a14092b55e.png)
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_class
的isa属性
是继承自objc_object
的,但在我们将其拷贝过来时去掉了objc_class
的继承关系,打印的结果会有问题如下图所示
![](https://img.haomeiwen.com/i1212147/30ba7dd1c8ee51b1.png)
加上isa属性
后,其正确的打印结果应该是这样的
![](https://img.haomeiwen.com/i1212147/d749e15386849c37.png)
再次添加代码如下,打印buckets
中的imp
和sel
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
是什么? - 为什么随着方法调用的增多,其打印的
occupied
和mask
会变化? -
bucket
数据为什么会有丢失的情况?
cache底层原理分析
- 首先从
cache_t
中的_mask属性
开始分析,找cache_t
中引起变化的函数,发现了incrementOccupied()
函数
![](https://img.haomeiwen.com/i1212147/cf2761532a4fa783.png)
incrementOccupied()
函数的具体实现如下
void cache_t::incrementOccupied()
{
_occupied++;
}
- 源码中全局搜索
incrementOccupied()
函数,发现只在cache_t
的insert
方法有调用 -
insert
方法理解为cache_t
的插入,而cache
中存储的就是sel-imp
,所以cache的原理从insert
方法开始分析,以下是cache原理分析的流程图
![](https://img.haomeiwen.com/i1212147/992ee324b698e5d4.png)
- 全局搜索
insert()
方法,发现只有cache_fill
方法中的调用符合 - 全局搜索
cache_fill
,发现在写入之前还有一步操作,即cache读取
,即查找sel-imp
疑问解答
-
_mask
是什么?
_mask
是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask
等于capacity - 1
-
_occupied
是什么?
_occupied
表示哈希表中sel-imp
的占用大小 (即可以理解为分配的内存中已经存储了sel-imp的的个数),
init会导致occupied变化
属性赋值,也会隐式调用,导致occupied变化
方法调用,导致occupied变化 -
为什么随着方法调用的增多,其打印的
occupied
和mask
会变化?
因为在cache初始化时,分配的空间是4个,随着方法调用的增多,当存储的sel-imp个数,即newOccupied + CACHE_END_MARKER(等于1)的和 超过 总容量的3/4,例如有4个时,当occupied等于2时,就需要对cache的内存进行两倍扩容 -
bucket
数据为什么会有丢失的情况?
原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的