引用计数管理
# 引用计数器
# 引用计数的存储策略
- 有些对象如果支持使用
Tagged Pointer,苹果会直接将其指针值作为引用计数返回; - 如果当前设备是
64位环境并且使用Objective-C 2.0,那么“一些”对象会使用其isa指针的一部分空间来存储它的引用计数; - 否则
Runtime会使用一张散列表来管理引用计数。
// objc.h
struct objc_object {
Class isa; // 在 arm64 架构之前
};
// objc-private.h
struct objc_object {
private:
isa_t isa; // 在 arm64 架构开始
};
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// nonpointer must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__ // 在 __arm64__ 架构下
# define ISA_MASK 0x0000000ffffffff8ULL // 用来取出 Class、Meta-Class 对象的内存地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
// 1:代表优化过,使用位域存储更多的信息
uintptr_t has_assoc : 1; // 是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1; // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t shiftcls : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
uintptr_t extra_rc : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
...... // 在 __x86_64__ 架构下
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
如果isa非nonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着Class、Meta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。
如果isa是nonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rc和has_sidetable_rc。
- extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,
has_sidetable_rc的值就会变为 1; - has_sidetable_rc:如果为 1,代表引用计数过大无法存储在
isa中,那么超出的引用计数会存储SideTable的RefCountMap中。
所以,如果isa是nonpointer,则对象的引用计数存储在它的isa_t的extra_rc中以及SideTable的RefCountMap中。
# Tagged Pointer
# isa指针
为什么既要使用一个extra_rc又要使用SideTables?
可能是因为历史问题,以前cpu是32位的,isa中能存储的引用计数就只有$2^7=128$。因此在arm64下,引用计数通常是存储在isa中的。
# SideTable
# alloc实现
经过一系列调用,最终调用了C函数calloc,此时并没有设置引用计数为1
此时并没有设置引用计数为1
# retain实现
SideTable &table = SideTables()[this];
//在tables里面,根据当前对象指针获取对应的sidetable
size_t &refcntStorage = table.refcnts[this];
//获得引用计数
//添加引用计数
refcntStorage += SIDE_TABLE_RC_ONE(4,位计算)
2
3
4
5
6
7
8
# release 实现
SideTable &table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find[this];
it->second -= SIDE_TABLE_RC_ONE
2
3
# retianCount
SideTable &table = SideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find[this];
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;(将向右偏移操作)
2
3
4
# 引用计数的获取
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
inline uintptr_t objc_object::rootRetainCount() {
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
// 加锁,用汇编指令ldxr来保证原子性
isa_t bits = LoadExclusive(&isa.bits);
// 释放锁,使用汇编指令clrex
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
//sidetable_retainCount()函数实现
uintptr_t objc_object::sidetable_retainCount() {
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
从上面的代码可知,获取引用计数的时候分为三种情况:
Tagged Pointer的话,直接返回isa本身;- 非
Tagged Pointer,且开启了指针优化,此时引用计数先从extra_rc中去取(这里将取出来的值进行了+1操作,所以在存的时候需要进行-1操作),接着判断是否有SideTable,如果有再加上存在SideTable中的计数; - 非
Tagged Pointer,没有开启了指针优化,使用sidetable_retainCount()函数返回。
# 总结
引用计数存在什么地方?
Tagged Pointer不需要引用计数,苹果会直接将对象的指针值作为引用计数返回;- 开启了指针优化(
nonpointer == 1)的对象其引用计数优先存在isa的extra_rc中,大于524288便存在SideTable的RefcountMap或者说是DenseMap中; - 没有开启指针优化的对象直接存在
SideTable的RefcountMap或者说是DenseMap中。
retain/release的实质
Tagged Pointer不参与retain/release;- 找到引用计数存储区域,然后+1/-1,并根据是否开启指针优化,处理进位/借位的情况;
- 当引用计数减为0时,调用
dealloc函数。
isa是什么
// ISA() assumes this is NOT a tagged pointer object Class ISA(); // getIsa() allows this to be a tagged pointer object Class getIsa();1
2
3
4
5- 首先要知道,isa指针已经不一定是类指针了,所以需要用
ISA()获取类指针; Tagged Pointer的对象没有isa指针,有的是isa_t的结构体;- 其他对象的isa指针还是类指针。
- 首先要知道,isa指针已经不一定是类指针了,所以需要用
对象的值是什么
- 如果是
Tagged Pointer,对象的值就是指针; - 如果非
Tagged Pointer, 对象的值是指针指向的内存区域中的值。
如何查找引用计数
,查找对象的引用计数表需要经过两次哈希查找:
- ① 第一次根据当前对象的内存地址,经过哈希查找从
SideTables()中取出它所在的SideTable; - ② 第二次根据当前对象的内存地址,经过哈希查找从
SideTable中的refcnts中取出它的引用计数
Q:为什么不是一个
SideTable,而是使用多个SideTable组成SideTables()结构?如果只有一个
SideTable,那我们在内存中分配的所有对象的引用计数或者弱引用都放在这个SideTable中,那我们对对象的引用计数进行操作时,为了多线程安全就要加锁,就存在效率问题。 系统为了解决这个问题,就引入 “分离锁” 技术方案,提高访问效率。把对象的引用计数表分拆多个部分,对每个部分分别加锁,那么当所属不同部分的对象进行引用操作的时候,在多线程下就可以并发操作。所以,使用多个SideTable组成SideTables()结构。- 如果是
# 补充: 一道多线程安全的题目
以下代码运行结果
@property (nonatomic, strong) NSString *target;
//....
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
});
}
2
3
4
5
6
7
8
9
答案:大概率地发生Crash。
Crash的原因:过度释放。
这道题看着虽然是多线程范围的,但是解题的最重要思路确是在引用计数上,更准确的来说是看对强引用的理解程度。关键知识点如下:
- 全局队列和自定义并行队列在异步执行的时候会根据任务系统决定开辟线程个数;
target使用strong进行了修饰,Block是会截获对象的修饰符的;- 即使使用
_target效果也是一样,因为默认使用strong修饰符隐式修饰; strong的源代码如下:
objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
2
3
4
5
6
7
8
9
假设这个并发队列创建了两个线程A和B,由于是异步的,可以同时执行。因此会出现这么一个场景,在线程A中,代码执行到了objc_retain(obj),但是在线程B中可能执行到了objc_release(prev),此时prev已经被释放了。那么当A在执行到objc_release(prev)就会过度释放,从而导致程序crash。
解决方法:
- 加个互斥锁
- 使用串行队列,使用串行队列的话,其实内部是靠
DISPATCH_OBJ_BARRIER_BIT设置阻塞标志位 - 使用weak
- Tagged Pointer,如果说上面的
self.target指向的是一个Tagged Pointer技术的NSString,那程序就没有问题。