回顾
之前的流程中ios 底层原理 03--isa 的走位图&类的结构探索 我们知道了类的基础结构是这样的 类由 isa
superclass
cache
bits
组成 cache_t
是结构体类型,有 2 个成员变量_bucketsAndMaybeMask
和一个union
联合体
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;
};
classDiagram
LGPerson --> cache_t
cache_t -->bucket_t
LGPerson : isa
LGPerson : superClass
LGPerson : cache
LGPerson : bits
class cache_t{
_buckets
_mask
_flags
_occupied
}
class bucket_t{
_sel
_imp
}
图片展示
cache_t的本质
在类的方法调用过程中,已知过程是通过SEL
(方法编号)在内存中查找IMP
(方法指针),为了使方法响应更加快速,效率更高,不需要每一次都去内存中把方法都遍历一遍,cache_t
结构体出现了。cache_t
将调用过的方法的SEL
和IMP
以及receiver
以bucket_t
结构体方式存储在当前类结构中,以便后续方法的查找。(PS:sel跟 imp 的关系 SEL:方法编号 SEL 相当于书本的目录名称(第几页的大纲)IMP:函数指针地址 IMP 相当于书的页码(第几页地址))
查看 cache_t的源码
在cache_t
看到了buckets()
,这个类似于class_data_bits_t
里面的提供的methods()
,都是通过方法获取值。
继续查看 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里面缓存的应该是方法
cache_t的流程图
classDiagram
objc_class --> cache_t真机
objc_class --> cache_t其他
cache_t其他 -->bucket_非真机
cache_t真机 -->maskAndBuckets说明
cache_t真机 -->cache_t中的mask和buckets
cache_t真机 -->bucket_真机
objc_class : Class isa
objc_class : Class superClass
objc_class : cache_t *cache
objc_class : class_data_bits_t bits
class cache_t真机{
uintptr_t _bucketsAndMaybeMask
mask_t _maybeMask
uint16_t _flags
uint16_t _occupied
capacity()
bucket_t *buckets()
mask_t occupied()
void incrementOccupied()
void setBucketsAndMask
void reallocate
void insert
}
class cache_t其他{
struct bucket_t *buckets()
mask_t mask
unit16_t flags
unit16_t occupied
}
class maskAndBuckets说明{
为了节省内存,读取方便mask和buckets存在一起
}
class bucket_非真机{
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
}
class cache_t中的mask和buckets{
maskShift = 48
maskZeroBits = 4
maxMask =((uintptr_t)(64 - maskShift)) - 1
static constexpr
(maskShift - maskZeroBits)) - 1
}
class bucket_真机{
explicit_atomic<SEL> _imp;
explicit_atomic<uintptr_t> _sel;
}
通过代码验证
创建LGPerson
类,自定义一些实例方法
,在main
函数中创建LGPerson
的实例化对象,然后进行lldb
调试
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;
- (void)saySomething;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson.h"
@implementation LGPerson
- (instancetype)init{
if (self = [super init]) {
self.name = @"Cooci";
}
return self;
}
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class pClass = [LGPerson class];
NSLog(@"%@",pClass);
}
return 0;
}
运行起来代码,并且通过 lldb 来进行调试
说明:LGPerson
对象没有调用对象方法,buckets
中没有缓存
方法的数据
在lldb中调用对象方法 继续lldb调试
在 buckets()里面寻找加入的saySomething
方法
总结:
- 调用
saySomething
后,_mayMask
和occupied
被赋值,这两个变量应该和缓存是有关系 bucket_t
结构提供了sel()
和imp(nil,pClass
方法saySomething
方法的sel
和imp
,保存在bucket
中,bucket
保存在cache
中
脱离源码环境分析cache
在 上面我们分析过 cache 的源码,那么我们可以仿照着重新写他的源码 代码如下
#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
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);
}
NSLog(@"Hello, World!");
}
return 0;
}
先只调用[p say1]
跟[p say2]
2021-07-14 16:16:30.205454+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : -[LGPerson say1]
2021-07-14 16:16:30.205773+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : -[LGPerson say2]
2021-07-14 16:16:30.205810+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : +[LGPerson sayHappy]
2021-07-14 16:16:30.205830+0800 003-cache_t脱离源码环境分析[19959:7458549] cache._occupied2 - cache._maybeMask3
2021-07-14 16:16:30.205903+0800 003-cache_t脱离源码环境分析[19959:7458549] _selsay1 - _imp0xb850f
2021-07-14 16:16:30.205937+0800 003-cache_t脱离源码环境分析[19959:7458549] _selsay2 - _imp0xb820f
2021-07-14 16:16:30.205965+0800 003-cache_t脱离源码环境分析[19959:7458549] _sel(null) - _imp0x0f
2021-07-14 16:16:30.205992+0800 003-cache_t脱离源码环境分析[19959:7458549] Hello, World!
在调用 say3 跟 say4
2021-07-14 16:18:31.427671+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say1]
2021-07-14 16:18:31.428242+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say2]
2021-07-14 16:18:31.428296+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say3]
2021-07-14 16:18:31.428329+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say4]
2021-07-14 16:18:31.428366+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : +[LGPerson sayHappy]
2021-07-14 16:18:31.428393+0800 003-cache_t脱离源码环境分析[19990:7460225] cache._occupied2 - cache._maybeMask7
2021-07-14 16:18:31.428497+0800 003-cache_t脱离源码环境分析[19990:7460225] _selsay4 - _imp0xb830f
2021-07-14 16:18:31.428547+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.428600+0800 003-cache_t脱离源码环境分析[19990:7460225] _selsay3 - _imp0xb800f
2021-07-14 16:18:31.428636+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.428670+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434070+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434127+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434160+0800 003-cache_t脱离源码环境分析[19990:7460225] Hello, World!
可以看到say1
和say2
方法消失了
cache
存储的位置是乱序
的比如say4
在最前面,第二与第四是空
_occupied
和_maybeMask
是什么缓存方法
是怎么插入到bucket
中的。
cache_t重要方法分析
cache_t的方法缓存的入口insert(SEL sel, IMP imp, id receiver)
,里面有参数sel
和imp
;而且还有方法名insert
,看看它的具体实现
总体流程:
occupied()
获取当前所占的容量,其实就是告诉你缓存中有几个bucket
了newOccupied
= occupied() + 1,表示你是第几个进来缓存的oldCapacity
目的是为了重新扩容的时候释放旧的内存- 只有
第一次
缓存方法的时,才会去开辟容量默认开辟容量是capacity
=INIT_CACHE_SIZE
即capacity = 4 就是4个bucket的内存大小 reallocate(oldCapacity, capacity, /* freeOld */false)
开辟内存,freeOld
变量控制是否释放旧的内存
reallocate方法探究
reallocate方法干了什么事情
- 1.
allocateBuckets
开辟内存 - 2.
setBucketsAndMask
设置mask
和buckets
的值 - 3.
collect_free
是否释放旧的内存,由freeOld
控制
allocateBuckets开辟内存方法
allocateBuckets方法主要做两件事:
calloc(bytesForCapacity(newCapacity)
, 1)开辟newCapacity * bucket_t
大小的内存end->set
将开辟内存的最后一个位置存入sel = 1
,imp
=第一个buket位置的地址
setBucketsAndMask方法
在源码里面有 3 个setBucketsAndMask
分别对应不同架构下的不同实现,但是功能都是向_bucketsAndMaybeMask
和 _maybeMask
写入数据
collect_free
主要是清空数据,回收内存
总结:
先计算当前所占的容量大小,然后开辟容量,当需要缓存的方法所占的容量小于总容量3/4
时就会直接走缓存流程
当容量超过3/4
,系统此时会进行两倍扩容
,扩容的最大容量不会超过mask的最大值2^15
扩容的时候会进行一步重要的操作,开辟新的内存
,释放回收旧的内存
,此时的freeOld = true
然后开始缓存方法
缓存方法
首先拿到bucket()
指向开辟这块内存首地址,也就是第一个bucket的地址,bucket()既不是数组也不是链表,只是一块连续的内存
hash函数
根据缓存sel
和mask
,计算出hash
下标。为什么需要mask
呢?mask
的实际作用是告诉系统你只能存前capacity - 1
中的位置,比如capacity = 4时,缓存的方法只能存前面3个空位
开始缓存
,当前的位置没有数据,就缓存
该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接return
。如果存在hash冲突
,下标一样,sel不一样,此时会进行再次hash
,冲突解决继续缓存