AutoreleasePool

AutoreleasePool实现原理

首先我们在 main.m 的文件中定义如下代码段:

我们可以通过如下两种方式转换这段代码

1. 利用xcode自带工具,转变成汇编语言如下所示

2. 通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令转换成c++

在main.m的同目录下会生成main.cpp文件,以下是截取文件中关键部分

通过对以上代码的转换后我们发现 AutoreleasePool在对象初始化时调用 _objc_autoreleasePoolPush / objc_autoreleasePoolPush 方法,在对象生命周期结束时调用 _objc_autoreleasePoolPop / objc_autoreleasePoolPop 方法。

通过runtime源码我们发现这些方法内部实现如下:

这两个方法最终调用了AutoreleasePoolPage对象的push()和pop()方法。所以调用了autorelease的对象最终都是转换通过AutoreleasePoolPage对象来管理的。

AutoreleasePoolPage 源码

AutoreleasePoolPage 内部结构如下所示(只截取了部分)

每个字段的含义如下:

1
2
3
4
5
6
7
8
9
10
11
12
POOL_BOUNDARY:原变量名 POOL_SENTINEL, 哨兵对象,用来区别每个page的边界
key:用于创建线程的标识
SCRIBBLE:刷子,可以不需要关心
SIZE:4096Bytes
COUNT:4096/8
magic:用于验证 AutoReleasePoolPage 的完整性
next:指向当前页中下一个能存放page对象的位置
thread: 当前页所在的线程
parent: 父节点, AutoReleasePoolPage 是一个双向链表,parent指向上一个节点
child: 子节点
depth:深度,表识这是第几个节点,从0开始计数
hiwat: high water mark 高水位线,用来报警内存占用

自动释放池中的栈

我们知道AutoreleasePoolPage是一个双向链表,它在内存中的结构如下所示:

我们发现以 POOL_BOUNDARY(POOL_SENTINEL)哨兵为界限,哨兵下面56bits的内存用于存储 AutoreleasePoolPage的成员变量,哨兵之后的内存都是用来存储加入到自动释放池中的对象。

主要方法说明

begin && end

1
2
3
4
5
6
7
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}

id * end() {
return (id *) ((uint8_t *)this+SIZE);
}

begin和end方法分别指向栈底和对象的结尾(见图中的begin和end)

add

1
2
3
4
5
6
7
8
9
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}

标准的进栈操作,向AutoreleasePoolPage添加对象

releaseUntil

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
void releaseUntil(id *stop) 
{

while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects

// 1. 获取hotpage(当前正在使用的page)
AutoreleasePoolPage *page = hotPage();

// 2. 判断是否为空,非空获取parent,并设置parent为hotpage
while (page->empty()) {
page = page->parent;
setHotPage(page);
}

page->unprotect();
// 3. 获取栈顶对象,清空栈顶指针,向栈顶对象发送release下移next指针
id obj = *--page->next;
// 4. 判断next是否等于stop,相等进入下一步,不等回到第一步
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();

if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
//5. 设置hotpage
setHotPage(this);

#if DEBUG
// debug模式每次会创建一个新page,每次pop会把page kill掉
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}

出栈操作,和普通的出栈不太一样,这里的出栈会一直递归出栈直到查找到 stop 才会停止。这里的SCRIBBLE 是用来填写空白区域内存的。

pageForPointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
AutoreleasePoolPage *result;
// 1. 获取偏移量
uintptr_t offset = p % SIZE;

assert(offset >= sizeof(AutoreleasePoolPage));

// 2. 通过当前p减去偏移量获取page指针
result = (AutoreleasePoolPage *)(p - offset);
// 3. 检测完整性
result->fastcheck();
// 4. 返回page
return result;
}

通过指针获取相对应的page

autoreleaseFast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline id *autoreleaseFast(id obj)
{
// 1. 获取hotpage(当前正在使用的page)
AutoreleasePoolPage *page = hotPage();
// 2.1 如果page存在且没满直接添加
// 2.2 如果page存在且满了,创建一个新page
// 2.3 如果page不存在,创建一个新page
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

快速获取page对象的指针

push()

1
2
3
4
5
6
7
8
9
10
11
12
static inline void *push() 
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

入栈操作,将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。分debug和release两种情况,debug下每次都会创建一个新的page,比release会多耗费内存。autoreleaseFast具体参见上一方法说明

pop(ctxt)

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
static inline void pop(void *token) 
{
AutoreleasePoolPage *page;
id *stop;

if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}

// 1. 找到token所在的页面
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}

if (PrintPoolHiwat) printHiwat();

// 2. 对token之后所有的对象发送release消息
page->releaseUntil(stop);

// 3. 杀掉多余的page(分debug模式和非debug模式)
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// 非debug模式
// 3.1 当page使用量小于一半时,杀掉page的child之后的所有page
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
// 3.2 当page使用量大约一半且存在孙子时,杀掉孙子之后的所有page
page->child->child->kill();
}
}
}

出栈操作,注意debug模式和非debug模式的不同。

小结

线程与runloop是一一对应的关系,因此不能是完成一次循环还是线程被关闭,autoreleasepool都会被释放。而 autorelease 是由若干 AutoreleasePoolPage 以双向链表的方式组成的, 每个page的大小是4096字节, 当程序运行到 @autoreleasepool{ 时, objc_autoreleasePoolPush()被调用, runtime会向当前的AutoreleasePoolPage中添加一个 POOL_BOUNDARY 对象作为哨兵, 在 {} 中创建的对象会被依次记录到AutoreleasePoolPage的栈顶指针, 当运行完 @autoreleasepool{} 时, objc_autoreleasePoolPop(哨兵)将被调用, runtime就会向AutoreleasePoolPage中记录的对象发送release消息直到哨兵的位置, 即完成了一次完整的运作.

以上内容参考以下文章

自动释放池的前世今生 —- 深入解析 autoreleasepool

AutoReleasePoolPage

在ARC环境中autoreleasepool(runloop)的研究