NSObject
底层的实现objc_class
,其初始化即对应底层一个objc_class
的一个初始化。那么objc_class
到底长什么样呢?
注意事项:本部分仅摘录了部分核心代码以节省篇幅。
一、objc_class基础结构
简化代码后如下
typedef struct objc_class *Class;
struct objc_object {
//实例对象的isa指向类对象;类对象的isa指向元类;元类的isa指向根元类(根元类的isa指向自己)
Class isa;
};
struct objc_class : objc_object {
//类对象的superclass指向父类(根类的superclass指向nil);元类的superclass指向父元类(根元类的superclass指向根类)
Class superclass;
//缓存等相关信息
cache_t cache;
//类对象、元类对象的信息
class_data_bits_t bits;
};
因为objc_class
继承自objc_object
,所以objc_class
也至少包括isa
、superclass
、cache
、bits
等4个成员变量。其中:
1.类对象的isa
指向元类;元类的isa
指向根元类(根元类的isa指向自己)。
2.类对象的superclass
指向父类(根类的superclass
指向nil);元类的superclass
指向父元类(根元类的superclass
指向根类)。
3.cache
记录缓存等相关信息。
4.bits
保存了类对象、元类对象的信息。
对于objc_class
的主要结构体关系图如下:
在开始探索之前,我们定义一个包含属性、成员变量、方法、协议(协议方法)的最小的类,示例代码如下,如下相关探索基本使用这个类展开:
@protocol NXPersonDelegate
- (void)eat;
@end
@interface NXPerson : NSObject{
NSInteger _uuid;
}
@property (nonatomic, copy) NSString *name;
- (void)run;
@end
@implementation NXPerson
- (void)run{
NSLog(@"%s",__func__);
}
- (void)eat{
NSLog(@"%s",__func__);
}
@end
二、class_data_bits_t、class_rw_t、class_ro_t的探索
简化后的class_data_bits_t如下:
struct class_data_bits_t {
uintptr_t bits; //8
}
class_data_bits_t
从定义上看包含bits
,占用8字节内存。bits&FAST_DATA_MASK
得到了class_rw_t
。class_rw_ext_t
和class_ro_t
中有我们熟悉的内容methods
、properties
等。class_ro_t
即所谓的clean memory
(后期是不会发生修改的),存放了编译器就已经确定的属性、方法、协议、变量,里面没有分类的方法。而在运行时会创建class_rw_t
即所谓的dirty memory
(随着程序的运行会进行一些增删),加载class
的时候首先会吧class_ro_t
中的属性、方法、协议拷贝到class_class_ext_t
中,然后将当前分类中的再拷贝进去,运行时方法、属性的查找也都是查找的class_class_ext_t
中的。
如上示例类我们定义了1个成员变量,1个属性,1个普通方法、1个协议方法、1个协议。根据属性=成员变量+set方法+get方法的原则,我们最终要找到2个成员变量(_uuid, _name)、1个属性(name)、4个方法(run, getName,setName:,eat)。
通过如上方式我们获取到类对象的
class_ro_t
结构体对应的变量$5
。接下来可以通过*$5.ivars
、*$5.baseProperties
、*$5.baseProtocols
、*(method_list_t *)$5.baseMethods()
获取到ro
中的成员变量、属性、协议、方法。
$6
中可以看到成员变量数量为2,打印得到_uuid
、_name
。
$9
中可以看到成员变量数量为5,打印得到name
、hash
、superclass
、description
、debugDescription
,包含了自定义的name
。
$11
中可以看到协议数量为1,打印得到NXPersonDelegate
。
$14
中可以看到方法数量为4,包括eat
、run
、name
、setName:
。
同理我们可以通过rw.methods()、rw.properties()、rw.protocols()获取到class_rw_ext_t
中存储的methods
、properties
、protocols
列表。调试方法与以上类似,在此不再重复。
三、cache_t的探索
简化后的cache_t如下:
struct cache_t {
explicit_atomic _bucketsAndMaybeMask;//8字节
union {
struct {
explicit_atomic _maybeMask;//4字节
uint16_t _flags; //2字节
uint16_t _occupied; //2字节
};
explicit_atomic _originalPreoptCache;//8字节
};
}
cache_t
从定义上看包含_bucketsAndMaybeMask
、_maybeMask
+_flags
+_occupied
与_originalPreoptCache
构成的联合体,占用16字节的内存。
通过官方源码中给的一段注释:
_bucketsAndMaybeMask is a buckets_t pointer, _maybeMask is the buckets mask
,我们推断这个指针指向的应该就是bucket_t内容,这个内容通过通过struct bucket_t *buckets() const;
获得。
我们通过lldb
命令看一下cache_t
的信息:
拿到cache_t后我们通过buckets()获取一下buckets:
根据打印的结果,可以看出
$2.buckets()[1]
中打印的_sel
是有内容的。那么他这个规律是什么样的呢?插入的策略是什么呢?源码中可能跟这个相关的是void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
方法,我们点进去看一下:
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask){
...
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
_maybeMask.store(newMask, memory_order_release);
...
}
这段代码主要就是如上2行代码,就是把newBuckets和newMask写入_bucketsAndMaybeMask
和_maybeMask
的操作。搜索是哪里调用了setBucketsAndMask(...)
,最终定位到:
ALWAYS_INLINE void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld){
bucket_t *oldBuckets = buckets();//获取当前的buckets
bucket_t *newBuckets = allocateBuckets(newCapacity);//按照新的大小区开辟内存
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);//设置新的buckets和_maybeMask。这里-1!!
if (freeOld) {
collect_free(oldBuckets, oldCapacity);//回收旧的内存
}
}
这一段代码主要就是先保存当前的buckets
的地址,然后按照新的大小开辟内存,最终将新的内存和大小(newCapacity-1)通过setBucketsAndMask
设置给_bucketsAndMaybeMask
和_maybeMask
,如果需要释旧的内存空间则调用collect_free
回收旧的内存空间。搜索是哪里调用了reallocate(...)
,最终定位到:
//本段代码是完整代码
void cache_t::insert(SEL sel, IMP imp, id receiver){
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s", cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity/*0*/, capacity/*4*/, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// 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) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
找到这个方法,有种发现太空奥秘的感觉,void cache_t::insert(SEL sel, IMP imp, id receiver)
函数名的意思就是插入sel+imp
,是它是它,准没错了!!!!!
我们分析一下他的流程(前置条件的判断这里就省略了):
- 1.1第一次进来
occupied()=0,则newOccupied = 1
;_maybeMask=0,则oldCapacity=0,capacity=0
; - 1.2.进入
if(isConstantEmptyCache){}
分支,capacity=0
,则capacity=INIT_CACHE_SIZE=4
,接着调用reallocate(oldCapacity/*0*/, capacity/*4*/, /* freeOld */false)
进行内存空间的开辟。根据上面的分析此时buckets
分配的是4 * bucket_t
的大小,_maybeMask = 3
。 - 1.3.获取当前的
buckets
,bucket_t *b = buckets();
,b = 4 * bucket_t
的大小。mask_t m = capacity - 1;
,则m=3
。mask_t begin = cache_hash(sel, m);
是对sel进行hash,这个函数内部计算hash
最后一步是value & mask
,也就是value&0B11
,也就是最终的结果(真是mask=3)只能是0
、1
、2
、3
这4种情况。这里假定是2则begin=2,I=2
。 - 1.4.进入
do-while
循环:检查buckets的索引为2的位置(从0开始)的sel是不是为空,如果为空执行incrementOccupied();
则_occupied=1
,并且执行b[i].set
向索引的2的位置写入(b, sel, imp, cls()); sel
和imp
。第一次进来这个条件肯定是满足的。 - 2.1 非第一次进来(假定第二次):
newOccupied=2,oldCapacity=4
、capacity = 4
。 - 2.2 进入
if(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)){}分支
,不做事情 - 2.3
m=4
, 初始的i = begin = sel
的hash
值。(i in [0,1,2,3]
)。 - 2.4 进入
do-while
循环:此时如果hash!=2,则按照第一的方式存储进去了(除了2号位置,其他位置没内容),如果找到的位置的sel跟当前的sel相同说明是相同的方法被调用了,不同重复缓存,直接跳出循环。反之则是hash冲突了。则执行cache_next()
重新计算hash
。(重新计算hash
不同平台不一样,__arm64__
架构下入锅之前的hash>=1
,则进行-1
操作,如果之前为0
,则直接返回mask;
其他则(hash+1)&mask)
。找到新的hash
赋值给i,然后继续尝试插入。如果不满足条件则继续寻找,直到又一次找到最初的位置,则推出while循环,打印bad_cache()
log信息。假定插入成功插在1号位置。 - 3.1 非第一次进入(假定第三次):
newOccupied=3,oldCapacity=4
,capacity = 4
。 - 3.2 进入
else{}
分支:capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
也就是在上一次的基础上翻倍了,且会检查capacity
不能大于MAX_CACHE_SIZE
。(其中MAX_CACHE_SIZE=0B1000 0000 0000 0000=0A32768
)。然后调用reallocate(oldCapacity/*4*/, capacity/*8*/, /* freeOld */true)
开辟新的内存。这里需要注意的是freeOld=true,也就是要把上一次保留的2个给他干掉了,也就是如果不调用之前的方法则缓存中就暂时没有了,后续如果有调用则会继续加入缓存。 - 3.3
m=8
, 初始的i = begin = sel
的hash
值。(i in [0,1,2,3,4,5,6,7]
)。 - 3.4 按照上面的规则查找
0-7
号合适的位置进行插入。 - ...
根据上面的规律,我们大概可以总结出:
1.申请开辟的内存是4、8、16... 等等 2的N次方,重新申请的是上一次的2倍空间,并且最大不能大于0A32768。源码中有标注Initial cache bucket count. INIT_CACHE_SIZE must be a power of two.
。
2.每次开辟内存空间的时候会把地址给_bucketsAndMaybeMask
,把mask=newCapacity-1
给_maybeMask
。并且每次重新开辟内存后也会把旧的buckets
的空间回收。
3.cache
只会缓存方法,缓存方法时会对方法进行hash
,如果相应的位置上可以插入则直接插入,如果这个位置上有相同的方法则不再重入插入,如果插入的位置有内容(hash
冲突),则通过cache_next
重新计算hash
,满足则插入,如果知道重新计算的hash
跟最初的hash
想等了则跳出循环。那么该方法就暂时缓存不了。
在源码环境做这些数据的调试很不方便,很多api是私有的我们我们只能通过地址区读取,中间只要一步错了,就需要从头再来,如果我们把一个类对应的结构体转换成我们自己定义的结构体是否可行呢?
第一步:仿照源码结构重新定义objc_class结构
struct NX_bucket {
SEL _sel;
IMP _imp;
};
struct NX_cache {
struct NX_bucket *_buckets;
uint32_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct NX_bits {
uintptr_t bits;
};
struct NX_objc_class {
Class isa;
Class superclass;
struct NX_cache cache;
struct NX_bits bits;
};
第二步:在原有的NXPerson的基础上增加了func1-func7共计7个方法:
@protocol NXPersonDelegate
- (void)eat;
@end
@interface NXPerson : NSObject {
NSInteger _uuid;
}
@property (nonatomic, copy) NSString *name;
- (void)run;
- (void)func1;
- (void)func2;
- (void)func3;
- (void)func4;
- (void)func5;
- (void)func6;
- (void)func7;
@end
@implementation NXPerson
- (void)eat{ NSLog(@"%s", __func__);}
- (void)run{NSLog(@"%s", __func__);}
- (void)func1{NSLog(@"%s", __func__);}
- (void)func2{NSLog(@"%s", __func__);}
- (void)func3{NSLog(@"%s", __func__);}
- (void)func4{NSLog(@"%s", __func__);}
- (void)func5{NSLog(@"%s", __func__);}
- (void)func6{NSLog(@"%s", __func__);}
- (void)func7{NSLog(@"%s", __func__);}
@end
第三部:编写测试代码:
- (void)test{
//将类对象强制转换成我们自定义的结构体。
struct NX_objc_class *__class = (__bridge struct NX_objc_class *)[NXPerson class];
//创建一个实例对象,后面会使用该实例对象调用实例方法。这里没有执行init方法,是为了排除init方法的干扰,init也会进行缓存。
NXPerson *p = [NXPerson alloc];
//顺序调用如下方法,每调用一个方法都打印一下当前的_maybeMask(当前开辟的内存容量大小)和_occupied(已占用的个数)
NSArray *funcs = @[@"func1", @"func2", @"func3", @"func4", @"func5", @"func6", @"func7", @"func8"];
for(int i = 0; i < funcs.count; i++){
NSString *func = funcs[i];
//调用方法。这里没有使用performSelector方法,是为了排除performSelector的干扰,performSelector也会进行缓存。
//这里需要把Build Settings中Enable Strict Checking of objc_msgSend Calls 设置为NO。
objc_msgSend(p, NSSelectorFromString(func));
//打印_maybeMask、_occupied
NSLog(@"\n\nfunc=%@;maybeMask=%d;occupied=%d", func, __class->cache._maybeMask, __class->cache._occupied);
//一次打印_buckets中的信息
for (int i = 0; i < __class->cache._maybeMask; i++){
struct NX_bucket bucket = __class->cache._buckets[i];
NSLog(@"index:%d,sel=%@,imp=%p", i,NSStringFromSelector(bucket._sel), bucket._imp);
}
}
}
打印结果较多,我们对打印的结果进行整理如下:
调用函数 | _maybeMask和_occupied | _buckets | 结果分析 |
---|---|---|---|
func1 | _maybeMask=3;_occupied=1 | index:0,_sel=(null),_imp=0x0 index:1,_sel=(null),_imp=0x0 index:2,_sel=func1,_imp=0xcfc0 |
新开内存为4,func1缓存在2号位置 |
func2 | _maybeMask=3;_occupied=2 | index:0,_sel=func2,_imp=0xcf90 index:1,_sel=(null),_imp=0x0 index:2,_sel=func1,_imp=0xcfc0 |
func1缓存在1号位置 |
func3 | _maybeMask=7;_occupied=1 | index:0,_sel=(null),_imp=0x0 index:1,_sel=(null),_imp=0x0 index:2,_sel=(null),_imp=0x0 index:3,_sel=(null),_imp=0x0 index:4,_sel=(null),_imp=0x0 index:5,_sel=(null),_imp=0x0 index:6,_sel=func3,_imp=0xcf60 |
2+1+1>4*3/4=>新开内存为8,func3缓存在6号位置 |
func4 | _maybeMask=7;_occupied=2 | index:0,_sel=(null),_imp=0x0 index:1,_sel=(null),_imp=0x0 index:2,_sel=(null),_imp=0x0 index:3,_sel=(null),_imp=0x0 index:4,_sel=func4,_imp=0xcf30 index:5,_sel=(null),_imp=0x0 index:6,_sel=func3,_imp=0xcf60 |
func4缓存在4号位置 |
func5 | _maybeMask=7;_occupied=3 | index:0,_sel=(null),_imp=0x0 index:1,_sel=(null),_imp=0x0 index:2,_sel=func5,_imp=0xcf00 index:3,_sel=(null),_imp=0x0 index:4,_sel=func4,_imp=0xcf30 index:5,_sel=(null),_imp=0x0 index:6,_sel=func3,_imp=0xcf60 |
func5缓存在2号位置 |
func6 | _maybeMask=7;_occupied=4 | index:0,_sel=func6,_imp=0xced0 index:1,_sel=(null),_imp=0x0 index:2,_sel=func5,_imp=0xcf00 index:3,_sel=(null),_imp=0x0 index:4,_sel=func4,_imp=0xcf30 index:5,_sel=(null),_imp=0x0 index:6,_sel=func3,_imp=0xcf60 |
func6缓存在0号位置 |
func7 | _maybeMask=7;_occupied=5 | index:0,_sel=func6,_imp=0xced0 index:1,_sel=func7,_imp=0xcea0 index:2,_sel=func5,_imp=0xcf00 index:3,_sel=(null),_imp=0x0 index:4,_sel=func4,_imp=0xcf30 index:5,_sel=(null),_imp=0x0 index:6,_sel=func3,_imp=0xcf60 |
func7缓存在1号位置 |
func8 | _maybeMask=15;_occupied=1 | index:0,_sel=(null),_imp=0x0 index:1,_sel=(null),_imp=0x0 index:2,_sel=(null),_imp=0x0 index:3,_sel=(null),_imp=0x0 index:4,_sel=func8,_imp=0xce70 index:5,_sel=(null),_imp=0x0 index:6,_sel=(null),_imp=0x0 index:7,_sel=(null),_imp=0x0 index:8,_sel=(null),_imp=0x0 index:9,_sel=(null),_imp=0x0 index:10,_sel=(null),_imp=0x0 index:11,_sel=(null),_imp=0x0 index:12,_sel=(null),_imp=0x0 index:13,_sel=(null),_imp=0x0 index:14,_sel=(null),_imp=0x0 |
5+1+1>8*3/4=>新开内存为16,func8缓存在4号位置 |
通过对测试数据的整理,我们更加深刻的明白底层扩容的实在容量即将达到3/4的时候进行扩容的,扩容后在新的容器中插入缓存数据,老得数据就丢弃了,这样也使得使用频率高的方法更容易被缓存下来,也是一种很好的淘汰机制啊!!