ARC 与 AutoReleasePool

ARC & MRC

“引用计数”是 iOS 系统管理堆内存的主要手段, 在早期 iOS 开发中, 引用计数是需要开发者手动管理, 称为 MRC.

MRC (Manual Reference Counting)

MRC 即手动管理内存, 系统是根据对象的引用计数来判断什么时候需要回收一个对象所占用的内存

  • 引用计数是一个整数
  • 从字面上, 可以理解为”对象被引用的次数”
  • 也可以理解为: 它表示有多少人正在用这个对象
  • 每个OC对象都有自己的引用计数
  • 任何一个对象, 刚创建的时候, 初始的引用计数为1
    • 当使用alloc、new或者copy创建一个对象时, 对象的引用计数默认就是1
  • 当没有任何人使用这个对象时, 系统才会回收这个对象, 也就是说
    • 当对象的引用计数为0时, 对象占用的内存就会被系统回收
    • 如果对象的计数不为0, 那么在整个程序运行过程, 它占用的内存就不可能被回收(除非整个程序已经退出 )

引用计数相关操作如下:

  • 为保证对象的存在, 每当创建引用到对象需要给对象发送一条retain消息, 可以使引用计数值+1 ( retain 方法返回对象本身)
  • 当不再需要对象时, 通过给对象发送一条release消息, 可以使引用计数值-1
  • 给对象发送retainCount消息, 可以获得当前的引用计数值
  • 当对象的引用计数为0时, 系统就知道这个对象不再需要使用了, 通过给对象发送dealloc消息释放它的内存
  • 需要注意的是: release并不代表销毁或者回收对象, 仅仅是计数减 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 只要创建一个对象默认引用计数的值就是1
Person *p = [[Person alloc] init];
NSLog(@"retainCount = %lu", [p retainCount]); // 1

// 只要给对象发送一个retain消息, 对象的引用计数就会 +1
[p retain];

NSLog(@"retainCount = %lu", [p retainCount]); // 2
// 通过指针变量p,给p指向的对象发送一条release消息
// 只要对象接收到release消息, 引用计数就会-1
// 只要一个对象的引用计数为0, 系统就会释放对象

[p release];
// 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数 -1
NSLog(@"retainCount = %lu", [p retainCount]); // 1

[p release]; // 0
NSLog(@"--------");
}
// [p setAge:20]; // 此时对象已经被释放
return 0;
}

当引用计数为 0 时候, 对象将会被系统销毁

对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此, 从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
dealloc方法的重写:

  • 一般会重写dealloc方法, 在这里释放相关资源, dealloc就是对象的遗言
  • 一旦重写了dealloc方法, (MRC)就必须调用[super dealloc] (ARC 无需调用 super), 并且放在最后面调用
1
2
3
4
5
6
7
- (void)dealloc
{
NSLog(@"Person dealloc");
// 注意:super dealloc一定要写到所有代码的最后
// 一定要写在dealloc方法的最后面
[super dealloc];
}

使用注意

  • 不能直接调用dealloc方法
  • 一旦对象被回收了, 它占用的内存就不再可用, 坚持使用会导致程序崩溃(野指针错误)

野指针和空指针

  • 只要一个对象被释放了, 我们就称这个对象为 “僵尸对象(不能再使用的对象)”
  • 当一个指针指向一个僵尸对象(不可用内存), 我们就称这个指针为野指针
  • 只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS错误)
1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init]; // 执行完引用计数为1

[p release]; // 执行完引用计数为0, 实例对象被释放
[p release]; // 此时, p就变成了野指针, 再给野指针p发送消息就会报错
[p release];
}
return 0;
}

为了避免给野指针发送消息会报错, 一般情况下, 当一个对象被释放后我们会将这个对象的指针设置为空指针, 空指针是没有指向存储空间的指针(里面存的是nil, 也就是0)

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init]; // 执行完引用计数为1

[p release]; // 执行完引用计数为0, 实例对象被释放
p = nil; // 此时, p变为了空指针
[p release]; // 再给空指针p发送消息就不会报错了
[p release];
}
return 0;
}

autorelease

当我们不再使用一个对象的时候应该将其空间释放, 但是有时候我们不知道何时应该将其释放, 比如方法的返回值. 为了解决这个问题, Objective-C 提供了autorelease方法.

使用autorelease有什么好处呢?

  • 不用再关心对象释放的时间
  • 不用再关心什么时候调用release

autorelease的原理实质上是什么?

autorelease实际上只是把对release的调用延迟了, 对于每一个autorelease, 系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release.

引用计数的存储

对象的引用计数一般存储在对象的isa指针中, 引用计数过大无法存储在 isa 中, 那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中.

了解 新版 isa 的实现, 需要先了解下nonpointer 概念

nonpointer
0: 代表普通的指针, 存储着 Class、Meta-Class 对象的内存地址
1: 代表优化过, 使用位域存储更多的信息
arm64 之后, isa 指针都是 nonpointer 了, 即 nonpointer: 1
arm64 之前的 isa 是非 nonpointer(未优化的指针), 引用计数存储在 SideTableRefCountMap
(本质是 指针从 32 位拓展到 64 位之后, 可以利用多出的 32 位存储额外数据, 加快访问速度)

isa 定义如下:

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
// objc-private.h
struct objc_object {
private:
isa_t isa;
// ...
}

// objc-private.h (objc 后续版本定义挪到了 isa.h)
union isa_t {
// ...
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)
};
// ...
}
  • isa_t中存储了两个引用计数相关的东西: extra_rchas_sidetable_rc
    • extra_rc: 里面存储的值是对象本身之外的引用计数的数量, 这 19 位如果不够存储, has_sidetable_rc的值就会变为 1;
    • has_sidetable_rc: 如果为 1, 代表引用计数过大无法存储在isa中, 那么超出的引用计数会存储SideTableRefCountMap中.

SideTable

1
2
3
4
5
6
7
// NSObject.mm
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表(散列表)
weak_table_t weak_table; // 弱引用表(散列表)
// ...
}

NSObject 类持有一个静态的 SideTables(), 本质也是一个散列表

1
2
3
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
  • 首先根据指针的地址做一次哈希, 知道当前指针的引用计数存在哪一个SideTable中(多个 SideTable 方便多线程访问)
    1
    2
    3
    4
    static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
  • 然后从 SideTablerefcnts 拿到引用计数

Tagged Pointer

在 iOS 引入 arm64 时, 还引入了 Tagged Pointer, 用于直接用指针存储一些小内存对象, 比如 NSNumber, 比较短的 NSString 等. 对于 64 位程序, 引入 Tagged Pointer 后, 相关逻辑能减少一半的内存占用, 以及3 倍的访问速度提升, 100倍的创建、销毁速度提升.

Tagged Pointer 定义如下

1
2
3
4
5
6
7
8
// objc-internal.h
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,

看下 iOS 上的 NSNumber

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];

NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @79;
NSNumber *number4 = @(0xFFFFFFFFFFFFFFFF);

NSLog(@"%p %p %p %p", number1, number2, number3, number4);
}
// 0xb000000000000012 0xb000000000000022 0xb0000000000004f2 0x600000678480

第一位b的二进制为1011, 其中第一位1Tagged Pointer标识位. 后面的011是类标识位, 对应十进制为3(OBJC_TAG_NSNumber), 表示NSNumber类.

  • 小的对象可以使用 Tagged Pointer, Tagged Pointer 在赋值时候不会产生 retain/release, 也就避免了多线程崩溃, 所以有时候大的字符串出现野指针崩溃, 但是小的字符串没问题, 也是这个原因

retainCount 源码

上面讲了怎么存储引用计数, 其实 retainCount 的获取就一目了然了. retainCount方法的函数调用栈为:

1
2
3
4
5
6
7
8
// 1. NSObject.mm
retainCount
// 2. objc-object.h
objc_object::rootRetainCount()
// 3. NSObject.mm
objc_object::sidetable_getExtraRC_nolock
objc_object::sidetable_retainCount

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
inline uintptr_t 
objc_object::rootRetainCount()
{
// 如果是 tagged pointer, 直接返回 this
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits); // 获取 isa
ClearExclusive(&isa.bits);
// 如果 isa 是 nonpointer
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc; // 引用计数 = 1 + isa 中 extra_rc 的值
// 如果还额外使用 sidetable 存储引用计数
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock(); // 加上 sidetable 中引用计数的值
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
// 如果 isa 不是 nonpointer, 返回 sidetable_retainCount() 的值
return sidetable_retainCount();
}

sidetable_getExtraRC_nolock 源码

1
2
3
4
5
6
7
8
9
10
11
size_t 
objc_object::sidetable_getExtraRC_nolock()
{
ASSERT(isa.nonpointer);
SideTable& table = SideTables()[this]; // 获得 SideTable
RefcountMap::iterator it = table.refcnts.find(this); // 获得 refcnts
if (it == table.refcnts.end()) return 0; // 如果没找到, 返回 0
else return it->second >> SIDE_TABLE_RC_SHIFT; // 如果找到了, 通过 SIDE_TABLE_RC_SHIFT 位掩码获取对应的引用计数
}

#define SIDE_TABLE_RC_SHIFT 2

sidetable_retainCount 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];

size_t refcnt_result = 1; // 设置对象本身的引用计数为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; // 引用计数 = 1 + SideTable 中存储的引用计数
}
table.unlock();
return refcnt_result;
}

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
ALWAYS_INLINE bool 
objc_object::rootRelease()
{
return rootRelease(true, false);
}

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
// 如果是 tagged pointer, 直接返回 false
if (isTaggedPointer()) return false;

bool sideTableLocked = false;

isa_t oldisa;
isa_t newisa;

retry:
do {
// 获取 isa
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// 如果 isa 不是 nonpointer
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
// 调用 sidetable_release
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果发现溢出的情况, 这里是下溢, 指 extra_rc 中的引用计数已经为 0 了
if (slowpath(carry)) {
// don't ClearExclusive()
// 执行 underflow 处理下溢
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits))); // 保存更新后的 isa.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
// extra_rc-- 下溢, 从 sidetable 借用或者 dealloc 对象
newisa = oldisa;

// 如果 isa 的 has_sidetable_rc 字段值为 1
if (slowpath(newisa.has_sidetable_rc)) {
// 如果 handleUnderflow == false, 调用 rootRelease_underflow
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.
// 将引用计数从 sidetable 中转到 extra_rc 中存储

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.
// 尝试从 sidetable 中删除(借出)一些引用计数, 传入 RC_HALF
// borrowed 为 sidetable 实际删除(借出)的引用计数
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.
// 为了避免竞争, has_sidetable_rc 必须保持设置
// 即使 sidetable 中的引用计数现在是 0

if (borrowed > 0) { // 如果 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
// 存储更改后的 isa.bits
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);
}
}
}
// 如果还是存储失败, 把引用计数再重新保存到 sidetable 中
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.
}
}

// 如果引用计数为 0, dealloc 对象
// Really deallocate.
// 如果当前 newisa 处于 deallocating 状态, 保证对象只会 dealloc 一次
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
// 调用 overrelease_error
return overrelease_error();
// does not actually return
}
// 设置 newisa 为 deallocating 状态
newisa.deallocating = true;
// 如果存储失败, 继续重试
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

// 如果 performDealloc == true, 给对象发送一条 dealloc 消息
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

可以看到执行到 if (borrowed > 0) 时候,

  • 如果 borrowed <= 0, 即表示最多只有 1 个指针指向此对象, 然后会执行objc_msgSend)(this, @selector(dealloc) 触发释放逻辑
  • 否则会 执行-1 然后 return 直接退出

dealloc 执行哪些操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline void
objc_object::rootDealloc()
{
// 判断是否为 TaggerPointer 内存管理方案, 是的话直接 return
if (isTaggedPointer()) return; // fixme necessary? *

if (fastpath(isa.nonpointer && // 如果 isa 为 nonpointer
!isa.weakly_referenced && // 没有弱引用
!isa.has_assoc && // 没有关联对象
!isa.has_cxx_dtor && // 没有 C++ 的析构函数
!isa.has_sidetable_rc)) // 没有额外采用 SideTabel 进行引用计数存储
{
assert(!sidetable_present());
free(this); // 如果以上条件成立, 直接调用 free 函数销毁对象
}
else {
object_dispose((id)this); // 如果以上条件不成立, 调用 object_dispose 函数
}
}

正常的释放逻辑 object_dispose 直接调用了 objc_destructInstance, 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
if (cxx) object_cxxDestruct(obj); // 如果有 C++ 的析构函数, 调用 object_cxxDestruct 函数
if (assoc) _object_remove_assocations(obj); // 如果有关联对象, 调用 _object_remove_assocations 函数, 移除关联对象
obj->clearDeallocating(); // 调用 clearDeallocating 函数
}

return obj;
}

clearDeallocating 还额外执行了 clearDeallocating_slow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

// 获取 SideTable
SideTable& table = SideTables()[this];
table.lock();
// 如果有弱引用
if (isa.weakly_referenced) {
// 调用 weak_clear_no_lock: 将指向该对象的弱引用指针置为 nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 如果有使用 SideTable 存储引用计数
if (isa.has_sidetable_rc) {
// 调用 table.refcnts.erase: 从引用计数表中擦除该对象的引用计数
table.refcnts.erase(this);
}
table.unlock();
}

总结下 dealloc 执行了

  1. 执行 C++ 析构
  2. 移除关联对象(_object_remove_assocations)
  3. 移除使用 SideTable 存储的引用计数
  4. 移除弱引用标记, 引用到的弱引用指针置为 nil
  5. free 此对象

weak 实现

在我们开发过程中, 经常需要使用弱引用, 来实现对一个对象可有可无地持有.
weak 弱引用是通过 objc_initWeak实现

1
2
3
4
5
6
7
8
9
10
11
12
id
objc_initWeak(id *location, id newObj) // *location 为 __weak 指针地址, newObj 为对象地址
{
// 如果对象为 nil, 那就将 weak 指针置为 nil
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

除了弱引用的初始化 还有设置弱引用/销毁等, 只是 storeWeak 的参数会有一些不一样

1
2
3
4
5
6
7
8
9
10
11
12
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
  1. objc_initWeak的参数 location指向弱引用指针, newObj为需要被弱引用的对象
  2. HaveOld 代表weak指针是否指向了一个弱引用
    1. DontHaveOld 表示之前未弱引用某个对象
    2. DoHaveOld 表示之前有弱引用某个对象
  3. HaveNew 代表weak指针是否需要只向一个新的弱引用
    1. DontHaveNew 表示不需要再弱引用一个新对象
    2. DoHaveNew 表示需要再弱引用一个新对象
  4. CrashIfDeallocating 代表的是被弱引用的对象是否在析构, 如果在析构会error.
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
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable; // 旧表, 用来存放已有的 weak 变量
SideTable *newTable; // 新表, 用来存放新的 weak 变量

// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
// 分别获取新旧值相关联的弱引用表
// 如果 weak 变量有旧值, 获取已有对象(该旧值对象)和旧表
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 如果有新值要赋值给变量, 创建新表
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

// 对 haveOld 和 haveNew 分别加锁
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

// 判断 oldObj 和 location 指向的值是否相等, 即是否是同一对象, 如果不是就重新获取旧值相关联的表
if (haveOld && *location != oldObj) {
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// 如果有新值, 判断新值所属的类是否已经初始化
// 如果没有初始化, 则先执行初始化, 防止 +initialize 内部调用 storeWeak 产生死锁
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);

// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;

goto retry;
}
}

// 如果有旧值, 调用 weak_unregister_no_lock 清除旧值
// Clean up old value, if any.
if (haveOld) {
// 移除所有指向旧值的 weak 引用, 而不是赋值为 nil
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// 如果有新值要赋值, 调用 weak_register_no_lock 将所有 weak 指针重新指向新的对象
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected

// 如果存储成功
// 如果对象是 Tagged Pointer, 不做操作
// 如果 isa 不是 nonpointer, 设置 SideTable 中弱引用标志位
// 如果 isa 是 nonpointer, 设置 isa 的 weakly_referenced 弱引用标志位
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

// 将 location 指向新的对象
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

store_weak函数的执行过程如下:

  • 分别获取新旧值相关联的弱引用表;
  • 如果有旧值, 就调用weak_unregister_no_lock函数清除旧值, 移除所有指向旧值的weak引用, 而不是赋值为nil;
  • 如果有新值, 就调用weak_register_no_lock函数分配新值, 将所有weak指针重新指向新的对象;
  • 判断isa是否为nonpointer来设置弱引用标志位. 如果不是nonpointer, 设置SideTable中的弱引用标志位, 否则设置isaweakly_referenced弱引用标志位.

再看看 weak_unregister_no_lock, 如果该指针之前已经弱引用了某个对象

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
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    //referent -- 被弱引用的对象
    //referrer -- weak指针的地址
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    weak_entry_t *entry;
    // 被弱引用的对象 , 不存在返回
    if (!referent) return;
    // 被弱引用的referent里面的weak_table中找到weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        remove_referrer(entry, referrer);//从weak_entry_t 中移除weak指针的地址
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false
                    break;
                }
            }
        }
        // 4张表中都不存在referrer 指针的地址,并且entry 中已经weak指针已被移除
        if (empty) {
            weak_entry_remove(weak_table, entry);//移除weak_table中的weak_entry_t
        }
    }
}

最后看看 weak_register_no_lock 真正设置弱引用的地方

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
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    //referent -- 被弱引用的对象
    //referrer -- weak指针的地址
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    //判断是否TaggedPointer 是返回referent_id
    if (_objc_isTaggedPointerOrNil(referent)) return referent_id;

    // 确保弱引用对象是可用的.
    if (deallocatingOptions == ReturnNilIfDeallocating ||
        deallocatingOptions == CrashIfDeallocating) {
        //...........................省略.............................//
    }
    weak_entry_t *entry;
    //被弱引用的referent里面的weak_table中找到weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);// 往weak_entry_t里插入referrer -- weak指针的地址
    } 
    else {
        //没找到weak_entry_t就新建一张
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);//在把new_entry插入到weak_table中
    }
    return referent_id;
}

ARC (Automatic Reference Counting)

ARC 是自动管理内存, WWDC2011 和 iOS5 引入. ARC 是新的 LLVM 3.0编译器的一项特性, 使用ARC, 可以说一举解决了广大iOS开发者手动内存管理的麻烦.

使用ARC后, 系统会检测出何时需要保持对象, 何时需要自动释放对象, 何时需要释放对象, 编译器会管理好对象的内存, 会自动插入retain, releaseautorelease, 通过生成正确的代码去自动释放或者保持对象. 我们完全不用担心编译器会出错.

使用 ARC 需要注意

  • 不允许调用对象的 release方法
  • 不允许调用 autorelease方法
  • 重写父类的dealloc方法时, 不能再调用 [super dealloc]

ARCMRC 的区别只是编译器会自动转换代码,

这点在 Objective-C Automatic Reference Counting (ARC) — Clang 16.0.0git documentation 文档中有提到, 通过汇编代码也可以分析:

objc_release 会减小对象的引用计数, 减小到 0 时对象就会被销毁, 假如这时有其它线程正在使用这个对象, 那么使用对象的线程就很可能发生崩溃.

崩溃场景

这一章节内容来自文章 头条稳定性治理: ARC 环境中对 Objective-C 对象赋值的 Crash 隐患
为了演示仅一行赋值代码就能造成崩溃, 以及清晰地分析崩溃的原因, 我设计了一个 Demo, 在 B 线程中释放 A 线程创建的对象使 C 线程崩溃:

复现过程:

  1. A、B、C 三个线程同时进入 foo 函数

  2. A 线程先创建初始值 _instance

    1. A 线程执行到 _instance = x0, 创建了新值并赋给 _instance;此时 _instance 引用计数为 1;
  3. B、C 线程读取到 A 线程创建的初始值 _instance

    1. B、C 线程分别执行到 x1 = _instance 时, 从 _instance 中读到线程 A 创建的对象, 保存到各自的上下文中;_instance 引用计数仍为 1;
  4. B 线程释放 _instance

    1. B 线程执行 objc_release(x1) 后会释放 _instance;_instance 引用计数变为 0, 被销毁;
  5. C 线程访问 _instance

    1. C 线程执行到 objc_release(x1) 时访问 _instance;由于 _instance 已经被销毁, 访问时会发生崩溃.

崩溃原因

如下图, 为什么会发生 EXC_BAD_ACCESS 崩溃?

ldr x17, [x2, #0x20] 指令认为寄存器 x2 中存放的是地址, 将该地址和 0x20 相加获得一个新地址, 再从新地址中读取 8 字节存放到 x17 中.

本例中可以分析出寄存器 x2 存放的是 Class 的地址, x2+0x20Class 的成员变量 bits 的地址, 这个地址是 0x00000007374040e0. 从这个地址中读值时操作系统发现它是非法内存地址, 从而产生 EXC_BAD_ACCESS 异常并报出这个错误地址.

附: Class 的结构体及成员变量的偏移

为什么 Class->bits 的地址会是 0x00000007374040e0 , 这个非法地址是怎么来的?
_instance 对象被销毁后, 内存被系统随机改写, 通过崩溃截图中 lldb 打印的日志可知:

  • 对象的 ISA 位置存放的随机值是 0x000010d7374040c0
  • Class = ISA & ISA_MASK = 0x00000007374040c0
  • Class->bits = 0x00000007374040c0 + 0x20 = 0x00000007374040e0

ISA 是随机值, 那么 Class、Class->bits 也都是随机值, 很容易是一个非法的内存地址, 访问非法内存地址就会产生 EXC_BAD_ACCESS 异常.

在执行 objc_release 函数之前 _instance 就已经销毁了, 为什么执行到 ldr x17, [x2, #0x20] 这一行指令时才发生崩溃, 之前没有崩溃?

EXC_BAD_ACCESS 异常发生在访问非法内存地址时. 在 ldr x17, [x2, #0x20] 之前仅有 ldr x16, [x0] 中使用方括号 [] 访问了 x0 中存储的地址. 此时 x0 中存储的是 _instance 的地址, _instance 销毁后对象的内存被系统随机改写, 而 x0 中的地址是之前就存进来的合法地址, 访问合法地址不会出现异常. (x0 存储的值指向的地址合法, 但是 x2+0x20 指向的地址不合法)

更多崩溃场景

崩溃在 objc_retain 中

崩溃原因: _instance 作为参数传递到 bar 函数, 在函数开始执行时会保留参数 objc_reatin(_instance), 结束执行时会释放参数objc_release(_instance). 若保留参数时 _instance 已被其它线程销毁, 就会导致崩溃在 objc_reatin 中.

崩溃在 objc_msgSend 中


崩溃原因: 第 7 行代码向 _instance 发送了 isEqual: 消息, 在执行到崩溃指令 ldr x11,[x16, #0x10] 时, 寄存器 x16 存放的是 _instanceClass, [x16, #0x10] 指令想要读取 Class->cache, 进而从 cache 中寻找缓存的方法. _instance 销毁后 ISAClassClass->cache 会成为随机值, 如果 Class->cache 是非法地址, 在执行 [x16, #0x10] 时就会崩溃.

崩溃在 objc_autoreleasePoolPop 中


崩溃原因: 若对象使用非 new/alloc/copy/mutableCopy 开头的接口创建, 并且不满足 Autorelease elision 策略, 会被添加到自动释放池中. 本例创建的 _instance 被添加到子线程的自动释放池中, 子线程任务执行完成后会对池中的对象 pop, 依次调用 objc_release 进行释放, 若次此时 _instance 已在其它线程中销毁, 就会发生崩溃.

EXC_BREAKPOINT 崩溃

除了上面提到的 EXC_BAD_ACCESS 异常, 这类问题也能导致其它类型的异常, 这里举一个 EXC_BREAKPOINT 异常的例子.

崩溃原因: -[NSString stringWithFormat:@"%@",_instance] 会调用 objc_opt_respondsToSelector 函数并将 _instance 作为参数传入. 在 objc_opt_respondsToSelector 函数发生崩溃前, x16 存储的是参数 _instanceClass.

ARM-指针认证 相关的指令会使 x16 寄存器与 x17 寄存器相等, 然后用 xpacd x17x17 寄存器中高位清零, 再比较 x16x17, 不相等则执行 brk 指令触发 EXC_BREAKPOINT 异常. xpacd 对合法指针清零不会改变指针的值, 不会执行 brk 指令产生异常. 当参数被销毁后, x16 可能被改写为非法指针并赋给 x17, xpacd x17 对非法指针高位清零会改变 x17, 使 x17 不等于 x16, 导致 EXC_BREAKPOINT 异常.

AutoReleasePool

iOS 程序开发的入口就与 Autoreleasepool 相关, 一般 main.m 文件是这样的

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

即 整个 iOS 的应用都是包含在一个自动释放池 block 中的.
其中 @autoreleasepool 是对 AutoReleasePool 的一个封装, 本质上是调用了 Push / Pop 方法, @autoreleasepool 帮助我们少写了这两行代码, 让代码看起来更美观

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// do whatever you want

objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

AutoreleasePool 是什么

1
2
3
4
5
6
7
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}

上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装.

AutoreleasePoolPage

AutoreleasePoolPage 是一个类, 主要定义如下:

1
2
3
4
5
6
7
8
9
class AutoreleasePoolPage {
magic_t const magic; // 对当前 AutoreleasePoolPage 完整性的校验
id *next; // 指向当前对象中下一个为空的内存地址, 或者下一个 AutoreleasePoolPage
pthread_t const thread; // 保存了当前页所在的线程
AutoreleasePoolPage * const parent; // 指向前一个 AutoreleasePoolPage
AutoreleasePoolPage *child; // 指向后一个 AutoreleasePoolPage
uint32_t const depth;
uint32_t hiwat;
};

每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的, 并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

1
2
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

双向链表

自动释放池中的 AutoreleasePoolPage 是以双向链表的形式连接起来的:

parent 和 child 就是用来构造双向链表的指针.

自动释放池中的栈

如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中, 它在内存中的结构如下:

  • 其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量, 剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象.
  • begin()end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址.
  • next 指向了下一个为空的内存地址, 如果 next 指向的地址加入一个 object, 它就会如下图所示移动到下一个为空的内存地址中:

POOL_SENTINEL(哨兵对象)

到了这里, 你可能想要知道 POOL_SENTINEL 到底是什么, 还有它为什么在栈中.

首先回答第一个问题: POOL_SENTINEL 只是 nil 的别名.

1
#define POOL_SENTINEL nil

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候, 都会把一个 POOL_SENTINEL push 到自动释放池的栈顶, 并且返回这个 POOL_SENTINEL 哨兵对象.

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, const char * argv[]) {
{
// atautoreleasepoolobj 就是一个 POOL_SENTINEL.
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// do whatever you want

objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

而当方法 objc_autoreleasePoolPop 调用时, 就会向自动释放池中的对象发送 release 消息, 直到第一个 POOL_SENTINEL:

  • 双向链表中第一个 page 的 begin() 一定是 POOL_SENTINEL
  • 每调用一次 objc_autoreleasePoolPush 会加入一个 POOL_SENTINEL
  • hotPage 指向双向链表中最后一个 page

objc_autoreleasePoolPush

objc_autoreleasePoolPush 最终会调用到 autoreleaseFast 函数(关键函数), 编辑器自动插入 autorelease 本质上也是调用的 autoreleaseFast.

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
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
// 在这里会进入一个比较关键的方法 autoreleaseFast, 并传入哨兵对象 POOL_SENTINEL
static inline void *push() {
// 表示压入一个 POOL_SENTINEL, 并不一定会创建新的 page
return autoreleaseFast(POOL_SENTINEL);
}

static inline id *autoreleaseFast(id obj)
{
// hotPage 可以理解为当前正在使用的 AutoreleasePoolPage.
AutoreleasePoolPage *page = hotPage();
// 有 hotPage 并且当前 page 不满
if (page && !page->full()) {
// 将对象添加至 AutoreleasePoolPage 的栈中
return page->add(obj);
} else if (page) { // 有 hotPage 并且当前 page 已满
// 1. 调用 autoreleaseFullPage 初始化一个新的页
// 2. 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
return autoreleaseFullPage(obj, page);
} else { // 无 hotPage
// 1. 调用 autoreleaseNoPage 创建一个 hotPage
// 2. 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
return autoreleaseNoPage(obj);
}
}

最后的都会调用 page->add(obj) 将对象添加到自动释放池中.

page->add 添加对象

代码简化后:

1
2
3
4
5
6
id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}

这个方法其实就是一个压栈的操作, 将对象加入 AutoreleasePoolPage 然后移动栈顶的指针(next).

autoreleaseFullPage(当前 hotPage 已满)

autoreleaseFullPage 会在当前的 hotPage 已满的时候调用:

1
2
3
4
5
6
7
8
9
10
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
// 遍历整个双向链表
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page); // 没有 next 就初始化一个
} while (page->full()); // 找到第一个没有满的 page

setHotPage(page); // 设置为当前在用的 page
return page->add(obj);
}

autoreleaseNoPage(没有 hotPage)

如果当前内存中不存在 hotPage, 就会调用 autoreleaseNoPage 方法初始化一个 AutoreleasePoolPage:

1
2
3
4
5
6
7
8
9
10
11
static id *autoreleaseNoPage(id obj) {
// 没有 hotPage 说明连双向链表都没有, 初始化第一个 page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page); // 设置为当前使用的 page

if (obj != POOL_SENTINEL) { // 如果不是哨兵对象
page->add(POOL_SENTINEL); // 先加一个哨兵对象
}

return page->add(obj);
}

objc_autoreleasePoolPop 方法

1
2
3
4
// ctxt 表示释放到哪一个指针为止, 一般传入的都是哨兵对象,
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage::pop简化版实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void pop(void *token) {
// 获取当前 token 所在的 AutoreleasePoolPage
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
// 调用 releaseUntil 方法释放栈中的对象, 直到 stop
page->releaseUntil(stop);

if (page->child) {
// 调用 child 的 kill 方法
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}

releaseUntil 释放对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void releaseUntil(id *stop) {
// 用一个 while 循环持续释放 AutoreleasePoolPage 中的内容
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();

while (page->empty()) {
page = page->parent;
setHotPage(page);
}
// 使用 memset 将内存的内容设置成 SCRIBBLE,
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
// 然后使用 objc_release 释放对象.
if (obj != POOL_SENTINEL) {
objc_release(obj);
}
}

setHotPage(this);
}

kill() 方法

将当前页面以及子页面全部删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void kill() {
AutoreleasePoolPage *page = this;
// 从最后一个 child 往回删除
while (page->child) page = page->child;

AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}

autorelease 方法

我们已经对自动释放池生命周期有一个比较好的了解, 最后需要了解的话题就是 autorelease 方法的实现, 先来看一下方法的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)

autorelease 方法的调用栈中, 最终都会调用上面提到的 autoreleaseFast 方法, 将当前对象加到 AutoreleasePoolPage 中. 上面已经详细解释过 autoreleaseFast了, 再贴一下源码吧:

1
2
3
4
5
6
7
8
9
10
11
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

AutoreleasePool 和 Runloop 的关系

Autorelease对象什么时候释放?

根据Autorelease机制, 在没有手加 AutoreleasePool 的情况下, Autorelease 对象是在当前的runloop 迭代结束时释放的, 而它能够释放的原因是 系统在每个runloop迭代中都加入了自动释放池Push和Pop.

  • 使用容器的block版本的枚举器时, 内部会自动添加一个 AutoreleasePool (但是 for infor ( ; ; i++) 没有)
  • 使用 GCD 的 block 方法时, 也会自动加 AutoreleasePool

根据苹果官方文档中对 Using Autorelease Pool Blocks 的描述, 我们知道在下面三种情况下是需要我们手动添加 autoreleasepool 的:

  1. 如果你编写的程序不是基于 UI 框架的, 比如说命令行工具;
  2. 如果你编写的循环中创建了大量的临时对象;
  3. 如果你创建了一个辅助线程.

子线程能用 Autorelease 吗?

我们知道主线程有 Runloop, 但是子线程是默认没有 Runloop 的, 那么在子线程能使用 Autorelease 吗? 能使用的话, 自动释放池是什么时候 Push 以及 Pop 的呢?

创建自动释放池

回顾一下上面的 objc_autoreleasePoolPush 会调用到 autoreleaseFast 函数, autoreleaseFast 获取了当前页 hotPage (AutoreleasePoolPage *page = hotPage();)

1
2
3
4
5
6
7
8
static inline AutoreleasePoolPage *hotPage() 
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}

这里的tls_get_direct实现如下, 相当于把 key 绑定到了子线程上

1
2
3
4
5
6
7
8
9
10
static inline void *tls_get_direct(tls_key_t k) 
{
assert(is_valid_direct_key(k));

if (_pthread_has_direct_tsd()) {
return _pthread_getspecific_direct(k);
} else {
return pthread_getspecific(k);
}
}

如果内存中不存在 hotPage, 就会走 autoreleaseNoPage 函数(AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);), 创建自动释放池中的第一个 page.

销毁自动释放池

自动释放池初始化的时候, 会监听线程退出然后调用tls_dealloc, 在tls_dealloc 调用 pop 函数清除自动释放池

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
class AutoreleasePoolPage 
{
// ...
static void init()
{
int r __unused = pthread_key_init_np(AutoreleasePoolPage::key,
AutoreleasePoolPage::tls_dealloc);
assert(r == 0);
}
// ...
static void tls_dealloc(void *p)
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
// No objects or pool pages to clean up here.
return;
}

// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);

if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) pop(page->begin()); // pop all of the pools
if (DebugMissingPools || DebugPoolAllocation) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}

// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}
}

线程退出的时候, 也会调用_pthread_tsd_cleanup函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
_pthread_tsd_cleanup(pthread_t self)
{
int i, j;
void *param;
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++)
{
for (i = 0; i < _POSIX_THREAD_KEYS_MAX; i++)
{
if (_pthread_keys[i].created && (param = self->tsd[i]))
{
self->tsd[i] = (void *)NULL;
if (_pthread_keys[i].destructor)
{
(_pthread_keys[i].destructor)(param);
}
}
}
}
}

该函数会对当前线程的TLS的资源进行清除,遍历所有pthread_key_t,调用其析构函数。我们知道autoreleasepool在线程中有对应的pthread_key_t, 因此,子线程中自动释放池的创建和释放都无需我们进行额外的操作。

Retain & Release 调用优化

这一章节内容来自文章 【WWDC22 110363】App 包大小优化和 Runtime 上的性能提升

Xcode 14 对于 RetainRelease 的包大小开销也进行了针对性的优化, 从之前的 8 个字节开销降低到了 4 个字节. Retain 和 Release 的调用和消息发送一样, 几乎也是无处不在的, 所以这项针对性的优化最高也可以带了 2% 的包大小优化收益. 但和消息发送不同, 这项优化依赖于 Runtime 的支持, 因此只有将最低支持版本迁移到 iOS 16、tvOS 16 或者是 watchOS 9 才能生效.

编译器在 ARC 时候会帮助我们插入大量的 retain / release 调用来确保程序中对象的内存何时释放.
每当我们通过 allocnewcopy 创建了一个对象后, 该对象的引用计数就会加一, 在底层是通过 _objc_retain 指令实现的.

参考 Advanced Memory Management Programming Guide 以及 Automatic Reference Counting - Swift

而当对象离开了自己的作用域, 需要被销毁的时候, 引用计数就会减一, 在底层是通过 _objc_release 指令实现的.

基于 ARC 计数, 编译器会帮助优化掉部分 _objc_retain_objc_release 指令调用. 但是如上图所示, 在方法最后结束前, 局部变量 caldateComponents 并没有作为返回值返回给方法的调用方, 因此需要被释放掉来实现内存回收.

在底层实现上, objc_retainobjc_release 都是普通的 C 函数, 接收唯一的参数 - 要被释放掉的对象. 而由于 ARC 的存在, 编译器会插入对这两个 C 函数的调用, 并传入对应的指针对象. 而为了遵循底层 ABI 定义的 C 函数的调用约定, 我们需要更多的代码执行这些调用来达到将对象指针传入正确的寄存器中的目的. 体现在汇编代码层面就是上图中各种 mov 指令.

自定义调用约定

Apple 基于此进行了针对性的「自定义调用约定」优化. 如上图所示, 通过自定义专门的 objc_retainobjc_release 调用约定, 我们可以根据对象指针的位置来使用正确的版本, 这样我们就可以避免额外的 mov 指令所带来的开销. 虽然这只是一个很细微的优化, 但正如我们前面所讲的, 对于整个 App 来说, 这项优化伴随着无处不在的 retainrelease 调用是有着量变引起质变的效果的.

Autorelease 自动省略

Apple 今年针对 Autorelease 自动省略的优化分为两个方面.

  • 受益于 Runtime 的更新, Autorelease 自动省略更高效.
  • 在此基础之上, 受益于编译器的更新, 最低发布版本为 iOS 16、tvOS 16 或者是 watchOS 9 的 App 会自动获得包大小减少 2% 的优化.

什么是 Autorelease 自动省略

在充分理解 Apple 针对 Autorelease 自动省略做出的优化前, 我们先简单温习一下什么是 Autorelease 自动省略.

还是基于之前的代码, 如上图所示, getWWDCDate 方法返回了临时创建的 theDate 对象. 然后 event 对象调用了 getWWDCDate 方法, 随后声明了 theWWDCDate 对象, 让其指向了方法的返回值 theDate 对象.

对于 getWWDCDate 方法的调用方, ARC 会插入 retain 语句.

而对于被调用方即 getWWDCDate 方会插入 release 语句, 因为 theDate 对象离开了其作用域. 但我们并不能真正马上就 releasetheDate 对象, 因为该对象并没有其它的引用.

如果我们在此时 release 了它, 在我们完成 getWWDCDate 方法的调用前 theDate 对象就会被销毁, 这并不是我们所期望的结果.

所以一个特殊的约定就是插入 autorelease 语句, 方法的调用方就可以紧接着对方法返回的对象进行 retain 操作.

事实上 Runtime 并不会保证真正的 release 操作何时发生, 但只要确保方法返回前临时对象不被销毁, 我们就可以在随后使用方法返回的临时对象并对其执行 retain 操作.

Autorelease 操作并不是没有开销的, Autorelease 省略就是专门来进行优化这项开销的. 要了解它是如何工作的, 让我们基于上面示例代码中的 return 语句查看其汇编实现.

当我们调用 autoreleease 之后, 我们视角就需要来到 Objective-C 运行时了, 此时有意思的事情就会发生了.

Objective-C 运行时需要知道我们正在返回一个 Autorelease 的对象. 而为了实现这一点, 编译器会生成一个我们不会用到的特殊标记. 通过这个特殊标记, 运行时就知道当前是否符合 Autorelease 省略的条件, 随之而来的是我们稍后要执行的 retain 操作.

如上图所示, 我们可以看到, 在 getWWDCDate 方法 return 的地方生成了对应的汇编指令 b _objc_autoreleaseReturnValue, 这里就相当于告诉了 Runtime 我们正在返回一个需要 Autorelease 的对象, 我们不会在方法返回前释放掉这个对象.

随后, 来到 getWWDCDate 方法的调用方, 对应的汇编指令是 bl _getWWDCDate, 前面我们已经提到过 bl 指令是带返回的跳转指令. 所以相当于是先执行的 bl _getWWDCDate 指令, 再执行的 b _objc_autoreleaseReturnValue 指令. 这就对应上了调用一个方法, 方法内部执行完成之后, 代码控制权又回到了方法调用方的流程.

紧随其后的是 mov x29, x29 指令, x29 表示是对通用寄存器 r29 的按 64 位访问操作. 而 r29 寄存器又被成为 fp(frame pointer) 寄存器. 了解栈结构和 iOS 内存分布的读者应该都知道栈是从高地址往低地址增长的, 而 fp 就指向了高地址, 也就是栈顶的位置. 但是乍看起来这句 mov x29, x29 似乎毫无意义, 因为这相当于将 fp 上的值移动到了 fp 上面. 这里的操作的意义并不在于 fp 的值移动, 而是本身这句指令的二进制存储以十六进制表示之后就是 0xAA1D03FD, 但是除此之外, 我们并没有得到有价值的信息.

单看这几条汇编指令可能不太清晰, 下面就让我们直接从源码入手来窥探底层的细节.

1
2
3
4
5
6
7
8
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id
objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

return objc_autorelease(obj);
}

代码来自于 objc4-818.2 源码中的 NSObject.mm 源文件.

我们可以看到, objc_autoreleaseReturnValue 函数会接收一个对象, 然后根据 prepareOptimizedReturn 方法返回值结果判断是否需要进行优化, 如果需要优化, 则增加一个 +1 的标志位, 不放入自动释放池中;如果不需要优化, 则会将对象放入自动释放池中, 等待下一次 RunLoop 结束后自动释放池的 Pop 操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Prepare a value at +0 for return through a +0 autoreleasing convention.
id
objc_retainAutoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;

// not objc_autoreleaseReturnValue(objc_retain(obj))
// because we don't need another optimization attempt
return objc_retainAutoreleaseAndReturn(obj);
}

// Same as objc_retainAutorelease but suitable for tail-calling
// if you don't want to push a frame before this point.
__attribute__((noinline))
static id
objc_retainAutoreleaseAndReturn(id obj)
{
return objc_retainAutorelease(obj);
}

代码来自于 objc4-818.2 源码中的 NSObject.mm 源文件.

同样的, objc_retainAutoreleaseReturnValue 方法也会判断传入的对象是否可以进行优化, 如果优化成功, 就增加一个 +0 标志位, 不放入自动释放池;如果不能优化, 则放入自动释放池中并 retain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition)
{
ASSERT(getReturnDisposition() == ReturnAtPlus0);

if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}

return false;
}

代码来自于 objc4-818.2 源码中的 objc-object.h 源文件.

重点来了, 关键函数是 callerAcceptsOptimizedReturn, 传入的参数值是__builtin_return_address(0).

  • __builtin_return_address

这个内建函数原型是 char *__builtin_return_address(int level) , 作用是得到函数的返回地址, 参数表示层数, 如 __builtin_return_address(0) 表示当前函数体返回地址, 传 1 是调用这个函数的外层函数的返回值地址, 以此类推.

由于 prepareOptimizedReturn 函数是 ALWAYS_INLINE 的, 所以该方法返回的就是(objc_autoreleaseReturnValue / objc_retainAutoreleaseReturnValue)的函数返回地址, 即变量创建的函数返回地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
// __arm__
# elif __arm64__

static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// fd 03 1d aa mov fp, fp
// arm64 instructions are well-aligned
if (*(uint32_t *)ra == 0xaa1d03fd) {
return true;
}
return false;
}

代码来自于 objc4-818.2 源码中的 objc-object.h 源文件.

显然, 在 callerAcceptsOptimizedReturn 函数的实现里面, 我们可以看到 mov fp, fp 汇编指令的真实代码是什么样的, 就是比较 ra 这个指针对象的值与 0xaa1d03fd 是否相等. 到这里我们就解答了上面 mov x29, x29 汇编指令的作用了.

Thread Local Storage(TLS)线程局部存储, 目的很简单, 将一块内存作为某个线程专有的存储, 以 key-value 的形式进行读写.
在返回值身上调用 objc_autoreleaseReturnValue 方法时, runtime 将这个返回值 object 储存在 TLS 中, 然后直接返回这个 object(不调用 autorelease);同时, 在外部接收这个返回值的 objc_retainAutoreleasedReturnValue 里, 发现 TLS 中正好存了这个对象, 那么直接返回这个 object(不调用 retain).
于是乎, 调用方和被调方利用 TLS 做中转, 很有默契的免去了对返回值的内存管理.
黑幕背后的 Autorelease
在 objc4 之前, objc_autoreleaseReturnValue / objc_retainAutoreleaseReturnValue 会将传入的 obj 利用 TLS 存储, 在 objc_retainAutoreleasedReturnValue / objc_unsafeClaimAutoreleasedReturnValue 中 根据 key 拿到原来的 objc 进行比较, 如果相同就什么都不做.
objc4 做了优化, 不再存储原始 obj, objc_autoreleaseReturnValue / objc_retainAutoreleaseReturnValue 只存储了一个标志位 ReturnDisposition, objc_retainAutoreleasedReturnValue / objc_unsafeClaimAutoreleasedReturnValue 根据标志位来进行判断是否需要 retain 或 release.
iOS 内存管理思考 - autorelease

接着, 运行时会以数据的形式加载这个特殊的标记指令 0xAA1D03FD, 然后比较是不是所期望的特殊标记指令从而达到 Autorelease 省略的效果. 关于 _objc_retainAutoreleasedReturnValue 的更多底层实现细节可以参考how-does-objc_retainautoreleasedreturnvalue-work 一文.

经过比较之后, 如果匹配成功, 则表示编译器告诉运行时我们正在返回一个随后马上会被 retain 的临时变量. 最后这就可以让我们达到省略或移除互相匹配的 autoreleaseretain 代码. 这就是 Autorelease 省略.

Autorelease 自动省略优化方案

但是由于将代码作为数据加载并不是一个十分通用的场景, 因此 CPU 并不会对此做出特殊优化.

让我们再次回到前面的例子上, 我们还是从 autorelease 作为探索的起点. 在这个时间点上, 我们已经有了一个十分有价值的线索, 那就是方法的「返回地址」. 通过方法「返回地址」, 我们就知道在方法执行完成之后需要执行到哪个地方. 所以我们可以持续追踪这个返回地址. 值得一提的是, 获取返回地址的操作十分得轻量, 因为返回地址只是一个指针, 我们可以存在一边以备后续流程使用.

接着我们将目光离开 autorelease , 回到方法的调用方, 当执行了 retain 操作之后, 我们重新回到了运行时中. 新的魔法开始了.

如上图绿色箭头所示, 在此时, 我们可以获取指向当前返回地址的指针.

随后, 我们只需要在运行时里面比较黄色箭头指针(之前执行 autorelease 操作时保存的函数地址)和绿色箭头的指针(执行 retain 操作时获得的函数返回地址)即可判断是否需要进行 Autorelease 省略. 因为我们这里进行的操作只是两个指针的比较, 这是十分轻量的操作. 我们不需要进行高昂的内存访问.

最重要的是, 我们不再需要通过以数据的形式加载特殊的标记指令来进行比较, 我们可以删除掉上图中 mov x29, x29 这条指令. 这让我们在代码上节省了一定的大小开销.

参考文章

  1. ios内存管理之weak
  2. AutoreleasePool详解和runloop的关系
  3. iOS 开发: 彻底理解 iOS 内存管理(MRC、ARC)
  4. 自动释放池的前世今生 —- 深入解析 autoreleasepool
  5. 头条稳定性治理: ARC 环境中对 Objective-C 对象赋值的 Crash 隐患
  6. Objective-C Automatic Reference Counting (ARC) — Clang 16.0.0git documentation
  7. WWDC22 - Improve app size and runtime performance
  8. 【WWDC22 110363】App 包大小优化和 Runtime 上的性能提升
  9. Advanced Memory Management Programming Guide
  10. Automatic Reference Counting - Swift
  11. 黑幕背后的 Autorelease
  12. how-does-objc_retainautoreleasedreturnvalue-work
  13. WWDC22: Improve app size and runtime performance - 掘金
  14. ARM-指针认证
  15. Using Autorelease Pool Blocks
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道