浅析AutoreleasePool源码

最近在拜读Draveness大佬的一篇文章自动释放池的前世今生 —- 深入解析 autoreleasepool,看到文中给读者留了一个问题:

我到现在也不是很清楚为什么要根据当前页的不同状态 kill 掉不同 child 的页面。

关于AutoreleasePool是什么,强力推荐阅读原文,写的很好。这里就不说了,直接讨论问题。

首先是整个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
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
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;
}

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();

page->releaseUntil(stop);

// 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) {
// hysteresis: keep one empty child if page is more than half fully
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

我们先看看释放的函数releaseUntil,它在释放的时候其实会一直顺着parent往前释放,直到参数stop,也就是说可能一次性释放好几个page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 代码有所删减
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();

while (page->empty()) {
page = page->parent;
setHotPage(page);
}

id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));

if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}

setHotPage(this);
}

然后我们来看看这段有疑问的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) { // 分支1
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) { // 分支2
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) { // 分支3
// hysteresis: keep one empty child if page is more than half fully
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}

这块代码的作用是删除空的子节点,释放内存。pop之后三种情况:

  1. 当前page为空,直接kill掉当前page,然后把parent设置为hotpage
  2. 当前page为空,而且没有parent,kill掉当前pagehotpage置为空;
  3. 当前page不为空,但是有child,如果当前page的空间占用不到一半,释放child,如果当前page的空间占用超过一半,且child还有child,直接释放这个孙子辈的page。(对于第三步注释中的解释是:keep one empty child if page is more than half fully)

我们再看看kill的实现,可以发现他是会顺着child一直往后释放,保证释放节点的child page都被释放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void kill()
{
AutoreleasePoolPage *page = this;
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);
}

到这里就可以得出结论了:

  1. pop之后,所有child page肯定都为空了,且当前page一定是hotPage
  2. 系统为了节约内存,判断,如果当前page空间使用少于一半,就释放掉所有的child page,如果当前page空间使用大于一半,就从孙子page开始释放,预留一个child page