OC底层原理35:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析

张建 lol

前言

本文主要是分析内存管理中的内存管理方案,以及 retain、retainCount、release、dealloc 的底层源码分析

ARC & MRC

iOS中的内存管理方案,大致可以分为两类:MRC(手动内存管理) 和 ARC(自动内存管理)

MRC

  • MRC 时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则

    • 对象被 创建时 引用计数都为 1

    • 当对象 被其他指针引用 时,需要手动调用 [objc retain],使对象的 引用计数+1

    • 当指针变量不再使用对象时,需要手动调用 [objc release]释放 对象,使对象的 引用计数-1

    • 当一个对象的 引用计数为0 时,系统就会 销毁 这个对象

  • 所以,在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理

ARC

  • ARC 模式是在WWDC2011和iOS5引入的自动管理机制,即 自动引用计数,是编译器的一种特性。其规则与MRC一致,区别在于,ARC模式下不需要手动retain、release、autorelease,编译器会在适当位置插入release和autorelease

内存布局

我们在前面介绍了 内存五大区,其实除了 内存区,还有 内核区保留区,以 4GB 手机为例,如下所示,系统将其中的 3GB 给了 五大区 + 保留区,剩余的 1GB 给内核区使用

  • 内核区:系统用来进行内核处理操作的区域

  • 五大区:这里不再作说明,具体请参考上面的链接

  • 保留区:预留给系统处理nil等

这里有个疑问,为什么五大区的最后内存地址是从 0x00400000 开始的。其主要原因是 0x00000000 表示 nil,不能直接用nil表示一个段,所以单独给了一段内存用于 处理nil 等情况

内存布局相关面试题

面试题1:全局变量和局部变量在内存中是否有区别?如果有,是什么区别?

  • 有区别

  • 全局变量 保存在内存的 全局存储区(即bss+data段),占用静态的存储单元

  • 局部变量 保存在 中,只有在所在 函数被调用时才动态的为变量分配存储单元

面试题2:Block中可以修改全局变量,全局静态变量,局部静态变量,局部变量吗?

  • 可以修改 全局变量,全局静态变量,因为 全局变量静态全局变量全局 的,作用域广

  • 可以修改局部静态变量,不可以修改局部变量

    • 局部静态变量(static修饰的)和 局部变量,被block从外面捕获的,成为 __main_block_imp_0 这个结构体的成员变量

    • 局部变量 是以 指针形式,被block捕获的,由于捕获的是指针,所以 可以修改 局部静态变量的值

  • ARC环境下,一旦使用 __block 修饰并在block中修改,就会 触发copy,block就会从 栈区copy到堆区,此时的 block是堆区block

  • ARC模式下,block中引用 id类型 的数据,无论有没有__block修饰,都会retain,对于 基础数据类型,没有__block就无法修改变量值,如果有 __block修饰,也是在底层修改 __block_byref_a_0 结构体,将其内部的 forwarding 指针指向 copy后的地址,来达到值的修改

内存管理方案

内存管理方案除了前文提及的 MRCARC,还有以下三种:

  • Tagged Pointer:专门用来处理小对象,例如NSNumber、NSDate、小NSString等

  • Nonpointer_isa:非指针类型的isa,主要是用来优化64位地址,这个在 OC底层原理08:isa和类关联探索 一文中,已经介绍了

  • SideTables散列表,在散列表中主要有两个表,分别是 引用计数表、弱引用表

这里主要着重借号 Tagged PointerSideTables,我们通过一个面试题来引入 Tagged Pointer

面试题

以下代码会有什么问题?

1
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
@interface ViewController ()
@property (nonatomic,strong)dispatch_queue_t queue;
@property (nonatomic,copy)NSString * nameStr;
@end

@implementation ViewController
- (void)taggedPointerDemo{
self.queue = dispatch_queue_create("com.zj.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10000; i ++) {
dispatch_async(self.queue, ^{
// alloc 堆 iOS优化 - taggedpointer
self.nameStr = [NSString stringWithFormat:@"zj"];
NSLog(@"%@",self.nameStr);
});
}
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i < 10000; i ++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"zj_越努力,越幸运"];
NSLog(@"%@",self.nameStr);
});
}
}
@end

运行以上代码,发现 taggedPointerDemo 单独运行没有问题,当触发 touchBegan 方法后。程序会崩溃,崩溃的原因是 多条线程同时对一个对象进行解释,导致了 过渡释放 所以崩溃。其根本原因是因为 nameStr 在底层的类型不一致导致的,我们可以通过调试看出

  • taggedPointerDemo 方法中的 nameStr 类型是 NSTaggedPointerString,存储在 常量区。因为 nameStralloc 分配时在 堆区,由于较小,所以经过xcode在iOS中的优化,成了 NSTaggedPointerString 类型,存储在 常量区

  • touchesBegan 方法中的 nameStr 类型是 NSCFString 类型,存储在 堆上

NSString的内存管理

我们可以通过NSString初始化的两种方式,来测试NSString的内存管理

  • 通过 WithString + @"" 方式初始化

  • 通过 WithFormat 方式初始化

1
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
#define ZJLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
// 初始化方式一:通过 WithString + @"" 方式
NSString * s1 = @"1";
NSString * s2 = [[NSString alloc] initWithString:@"2"];
NSString * s3 = [NSString stringWithString:@"3"];

ZJLog(s1);
ZJLog(s2);
ZJLog(s3);

// 初始化方式二:通过 WithFormat
// 字符串长度在9以内
NSString * s4 = [NSString stringWithFormat:@"123456789"];
NSString * s5 = [[NSString alloc] initWithFormat:@"123456789"];

// 字符串长度大于9
NSString * s6 = [NSString stringWithFormat:@"1234567890"];
NSString * s7 = [[NSString alloc] initWithFormat:@"1234567890"];

ZJLog(s4);
ZJLog(s5);
ZJLog(s6);
ZJLog(s7);
}

以下是运行的结果

所以,从上面可以总结出,NSString的 内存管理 主要分为3中

  • __NSCFConstantString:字符串常量区,是一种 编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区

  • __NSCFString:是在 运行时 创建的 NSString子类,创建后 引用计数会加1,存储在堆上

  • NSTaggedPointerString:标签指针,是苹果在64位环境下对 NSString、NSNumber 等对象做的优化,对于NSString对象来说

    • 字符串是由数字、英文字母组合且长度小于等于9 时,会自动成为 NSTaggedPointerString 类型,存储在 常量区

    • 当有 中文或其他特殊符号 时,会直接成为 __NSCFString 类型,存储在 堆区

Tagged Pointer 小对象

由一个NSString的面试题,引出了 Tagged Pointer ,为了探索小对象的引用计数器,处理我们需要进入 objc 源码中查看 retain、release 源码中对 Tagged Pointer 小对象的处理

小对象的引用计数处理分析

  • 查看 setProperty -> reallySetProperty 源码,其中是对 新值retain旧值release

  • 进入 objc_retain、objc_release 源码,在这里都判断是否是小对象,如果是小对象,则不会进行retain和release,会直接返回,因此可以得出一个结论:如果是小对象,不会进行retain和release
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
//判断是否是小对象,如果是,则直接返回对象
if (obj->isTaggedPointer()) return obj;
//如果不是小对象,则retain
return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
//如果是小对象,则直接返回
if (obj->isTaggedPointer()) return;
//如果不是小对象,则release
return obj->release();
}

小对象地址分析

  • 一般的 NSString 对象指针,都是 string值 + 指针地址,两者是 分开

  • 对于 Tagged Pointer 指针,其 指针+值,都能在小对象中体现,所以 Tagged Pointer即包含指针,也包含值

在之前的文章讲类加载时,其中的 _read_image 源码中有一个方法对小对象进行了处理,即 initializeTaggedPointerObfuscator 方法

  • 进入 _read_image -> initializeTaggedPointerObfuscator 源码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void
initializeTaggedPointerObfuscator(void)
{

if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
}
//在iOS14之后,对小对象进行了混淆,通过与操作+_OBJC_TAG_MASK混淆
else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}

在实现中,我们可以看出,在iOS14之后,Tagged Pointer 采用了混淆处理,如下所示

  • 我们可以在源码中通过 objc_debug_taggedpointer_obfuscator 查找 taggedPointer 的 编码 和 解码 ,来查看底层是如何混淆处理的
1
2
3
4
5
6
7
8
9
10
11
//编码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;

通过实现,我们可以得知,在编码和解码部分,经过了 两层异或,其目的是 得到小对象自己,例如以 1010 0001 为例,假设 mask1010 1000

1
2
3
4
5
 1010 0001 
^0101 1000 mask(编码)
1111 1001
^0101 1000 mask(解码)
1010 0001
  • 所以在外界,为了 获取小对象的真是地址,我们可以将解码的源码拷贝到外面,将NSString混淆部分进行 解码,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern uintptr_t objc_debug_taggedpointer_obfuscator;

- (void)test{
NSString * s1 = [NSString stringWithFormat:@"a"];
NSString * s2 = [NSString stringWithFormat:@"b"];

NSLog(@"%p - %@",s1,s1);
NSLog(@"%p - %@",s2,s2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(s2));

}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

打印结果如下


观察解码后的 小对象地址,其中的 10 表示 bASCII 码,再以 NSNumber 为例,同样可以看出,8 就是我们实际的值

1
2
3
NSNumber * n1 = @1;

NSLog(@"%@ - %p - 0x%lx",n1,n1,_objc_decodeTaggedPointer_(n1));

打印结果如下

到这里,我们验证了 小对象指针地址中确实存储了值

TaggedPointer 总结

  • Tagged Pointer 小对象类型(用于存储 NSNumber、NSDate、小NSString),小对象指针不再是简单的地址,而是 地址+值,即 真正的值,所以,实际上 它不再是一个对象了,它只是一个披着对象皮的普通变量 而已。所以可以直接进行读取。优点是 占用空间小、节省内存

  • Tagged Pointer 小对象 不会进入retain和release,而是 直接返回了,意味着 不需要ARC进行管理,所以 可以直接被系统自主的释放和回收

Tagged Pointer内存并不存储在堆 中,而是在 常量区 中,也 不需要malloc和free,所以可以直接读取,相比存储在堆区的数据 读取效率上快乐3倍左右创建 的效率相比堆区 快了近100倍左右

  • 所以,综合来说,Tagged Pointer 的内存管理方案,比常规的内存管理,要快的多

  • Tagged Pointer 的64位地址中,前4 位代表类型,后4 位主要适用于系统做一些处理,中间56位用于存储值

  • 优化内存建议:对于 NSString 来说,当字符串 较小 时,建议直接通过 @"" 初始化,因为存储在 常量区,可以直接进行读取,会比 WithFormat初始化方式 更加快速

SideTable 散列表

引用计数 存储到一定值时,并不会在存储到 Nonpointer_isa 的位域的 extra_rc 中,而是会存储到 SideTable散列表中

下面我们就来继续探索引用计数retain的底层实现

retain源码分析

  • 进入 objc_retain -> retain -> rootRetain 源码实现,主要有以下几部分逻辑

    • 【第一步】判断是否为 Nonapointer_isa

    • 【第二步】操作引用计数

      • 如果不是 Nonapointer_isa,则直接操作 SideTable 散列表,此时的散列表并不只有一张,而是有很多张(后续会分析,为什么需要多张?)

      • 判断 是否正在释放,如果正在释放,则执行dealloc流程

      • 执行 extra_rc + 1,即引用计数+1操作,并给一个引用计数的 状态标识carry, 用于表示 extra_rc 是否满了

      • 如果 carry 的状态表示 extac_rc的引用计数满了 ,此时需要操作 散列表,即满状态拿出来存到 extra_rc,另一半存在散列表的 extrc_half。这么做的原因是因为如果都存储在 散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么 对半分 操作的目的在于 提高性能

1
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
53
54
55
56
57
58
59
60
61
62
63
64
65
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;
//为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
//重点
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否为nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa,直接操作散列表sidetable
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//dealloc源码
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}


uintptr_t carry;
//执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断extra_rc是否满了,carry是标识符
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc
newisa.extra_rc = RC_HALF;
//给一个标识符为YES,表示需要存储到散列表
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

问题1:散列表为什么在内存有多张?最多能够多少张?

  • 如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着 数据不安全

  • 如果 每个对象都开一个表,会 耗费性能,所以也不能有无数个表

  • 散列表的类型是 SideTable,有如下定义

1
2
3
4
5
6
7
struct SideTable {
spinlock_t slock;//开/解锁
RefcountMap refcnts;//引用计数表
weak_table_t weak_table;//弱引用表

....
}
  • 通过查看 sidetable_unlock 方法定位 SideTables,其内部是通过 SideTablesMap 的get方法获取。而 SideTablesMap 是通过 StripedMap<SideTable> 定义的
1
2
3
4
5
6
7
8
9
10
11
12
13
void 
objc_object::sidetable_unlock()
{
//SideTables散列表并不只是一张,而是很多张,与关联对象表类似
SideTable& table = SideTables()[this];
table.unlock();
}
👇
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
👇
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

从而进入 StripedMap 的定义,从这里可以看出,同一时间,真机中散列表最多只能有8张

问题2:为什么在用散列表,而不用数组、链表?

  • 数组:特点在于 查询方便(即通过下标访问),增删比较麻烦(类似于之前讲过的 methodList,通过 memcopy、memove 增删,非常麻烦),所以数据的特性是 读取快、存储不方便

  • 链表:特点在于 增删方便、查询慢(需要从头头节点开始遍历查询),所以链表的特性是 存储快、读取慢

  • 散列表本质 就是一张 哈希表,哈希表 集合了数组和链表的长处,增删改查都比较方便,例如 拉链哈希表(在之前锁的文章中,讲过 tls 的存储结构就是 拉链形式的),是最常用的,如下所示

可以从 SideTables -> StripedMap -> indexForPointer 中验证是 通过嘻哈函数计算哈希下标 以及 sideTables 为什么可以 使用[] 的原因

所以,综上所述,retain 的底层流程如下所示

总结:retain完成回答

  • retain 在底层首先会 判断是否是 Nonpointer isa,如果 不是,则直接操作散列表,进行+1操作

  • 如果 是Nonpointer isa,还需要 判断是否正在释放,如果正在 释放,则执行dealloc流程,释放弱引用表和引用技术表,最后free释放对象内存

  • 如果 不是正在释放,则对Nonpointer isa进行常规引用计数+1,这里需要注意一点的是,extra_rc 在真机上只有 8位用于存储引用计数的值,当存储 满了 时,需要 借助散列表 用于存储。需要将满了 extra_rc 对半分,一半(即2^7)存储在 散列表 中,另一半还是存储在 extra_rc 中,用于常规的引用计数的+1或者-1操作,然后再返回

release 源码分析

分析了 retain 的底层实现,下面来分析 release 的底层实现

  • 通过 setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease 顺序,进入 rootRelease 源码,其操作与retain相反

    • 判断是否是 Nonpointer isa ,如果不是,则 直接对散列表进行-1操作

    • 如果是 Nonpointer isa ,则对extra_rc 中的引用计数值进行 -1 操作,并存储此时的 extra_rc 状态到 carry 中

    • 如果此时的状态 carry 为 0,则走到 underflow 流程

    • underflow 流程有以下几步

      • 判断 散列表是否存储了一半的引用计数

      • 如果是,则从 散列表 中取出 存储的一半引用计数,进行 -1操作,然后存储到 extra_rc

      • 如果此时 extra_rc 没有值,散列表中也是空的,则直接进行析构,即 dealloc 操作,属于自动触发

1
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;

bool sideTableLocked = false;

isa_t oldisa;
isa_t newisa;

retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,则直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//进行引用计数-1操作,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此时extra_rc的值为0了,则走到underflow
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa;
//判断散列表中是否存储了一半的引用计数
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}

// Try to remove some retain counts from the side table.
//从散列表中取出存储的一半引用计数
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.

if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//进行-1操作,然后存储到extra_rc中
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
//此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
// Really deallocate.
//触发dealloc的时机
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

if (performDealloc) {
//发送一个dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

所以,综上所述,release 的底层流程如下图所示

dealloc 源码分析

retainrelease 的底层实现中,都提及了 dealloc 析构函数,下面来分析 dealloc 的底层实现

  • 进入 dealloc -> _objc_rootDealloc -> rootDealloc 源码实现,主要有两件事:

    • 根据条件 判断是否有isa、cxx、关联对象、若引用表、引用计数表,如果没有,则 直接free释放内存
    • 如果有,则进入 object_diapose 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline void
objc_object::rootDealloc()
{
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
if (isTaggedPointer()) return; // fixme necessary?

//如果没有这些,则直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//如果有
object_dispose((id)this);
}
}
  • 进入 object_dispose 源码,其目的有以下几个

    • 销毁实例,主要有以下操作

      • 调用c++析构函数

      • 删除关联引用

      • 释放散列表

      • 清空弱引用表

    • free释放内存

1
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
id 
object_dispose(id obj)
{
if (!obj) return nil;
//销毁实例而不会释放内存
objc_destructInstance(obj);
//释放内存
free(obj);

return nil;
}
👇
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}

return obj;
}
👇
inline void
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是,则直接释放散列表
sidetable_clearDeallocating();
}
//如果是,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}
👇
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}

所以,综上所述,dealloc 底层的流程图如图所示

所以,到目前为止,从最开始的 alloc底层分析 -> retain -> release -> dealloc 就全部串联起来了

retainCount 源码分析

引用计数的分析通过一个面试题来说明

面试题:alloc创建的对象的引用计数为多少?

  • 定义如下代码,打印其引用计数
1
2
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

其打印结果如下

  • 进入 retainCount -> _objc_rootRetainCount -> rootRetainCount 源码,其实现如下:
1
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
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
👇
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);

return obj->rootRetainCount();
}
👇
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa,才有引用计数的下层处理
if (bits.nonpointer) {
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
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();
}

在这里我们可以通过源码断点调试,来查看此时的 extra_rc 的值,结果如下

答案: 综上所述,alloc 创建的对象 实际的引用计数为0,其引用计数打印结果为1,是因为在底层 rootRetainCount 方法中,引用计数默认+1 了,但是这里 只有 对引用计数的 读取 操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然诶0

总结

  • alloc 创建的对象 没有retain 和 release

  • alloc 创建对象的 引用计数为0,会在 编译时期,程序 默认加1,所以读取引用计数时为1

  • Post title:OC底层原理35:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
  • Post author:张建
  • Create time:2021-05-22 14:47:27
  • Post link:https://redefine.ohevan.com/2021/05/22/OC底层原理/OC底层原理35:内存管理(一)TaggedPointer-retain-release-dealloc-retainCount-底层分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.