底层初窥——AutoReleasePool

@autoreleasepool 是什么?

我们可以在 runtime 源码 NSObject.mm 中找到官方对自动释放池实现的介绍:

A thread’s autorelease pool is a stack of pointers.

Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.

A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.

The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.

Thread-local storage points to the hot page, where newly autoreleased objects are stored.

大概的意思就是:

  • 自动释放池是栈结构,存储的是指针。

  • 指针指向需要自动释放的对象或者 POOL_BOUNDARY 边界值。以 POOL_BOUNDARY 为边界,当释放池释放时,在边界之内的对象会被释放。

  • page 以双向链表的形式构成 pool,page 会自动创建或释放。

  • 线程本地存储指向当前最新的 page。

接下来,我们从代码来慢慢探索具体实现。

1
2
3
4
int main(int argc, const char * argv[]) {
@autoreleasepool {}
return 0;
}
  • 使用 clang -rewrite-objc main.m 查看经过编译器前端处理的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

可以看出 @autoreleasepool{} 会创建一个__AtAutoreleasePool 类型的局部变量并包含在当前作用域,__AtAutoreleasePool 构造和析构时分别调用了两个方法,所以简化过程如下:

1
2
3
4
5
6
7
8
9
10
int main(int argc, char * argv[]) {
/* @autoreleasepool */ {
//创建自动释放池
__AtAutoreleasePool __autoreleasepool = objc_autoreleasePoolPush();
//TODO 执行各种操作,将对象加入自动释放池

//释放自动释放池
objc_autoreleasePoolPop(__autoreleasepool)
}
}
  • push 和 pop
1
2
3
4
5
6
7
8
9
10
11
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

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

这两个函数其实都是对 AutoreleasePoolPage 的封装。接下来我们来看看 AutoreleasePoolPage 的结构及实现。

AutoreleasePoolPage

可以在 runtime 源码 NSObject.mm 中找到 AutoreleasePoolPage 的源码(下面为简化代码):

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
class AutoreleasePoolPage 
{
......
// 当一个释放池刚 push 且其中未存储任何对象时,
// EMPTY_POOL_PLACEHOLDER 存储在 TLS
// 当顶层(即libdispatch)在推送和弹出池,但从不使用它们时,这可以节省内存。

// 占位符,当 pool 为空时存储的指针
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
// 边界值
# define POOL_BOUNDARY nil

// TLS 存储时的 key 值
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing

// 大小 可根据宏定义查得数值为:4096
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
// page 可存储的对象数量
static size_t const COUNT = SIZE / sizeof(id);

magic_t const magic; // 用于校验内存是否损坏
id *next; // 指向当前可插入对象的地址
pthread_t const thread; // 指向当前 page 对应的线程
AutoreleasePoolPage * const parent; // 指向前驱指针
AutoreleasePoolPage *child; // 指向后继指针

......
}

由上面 AutoreleasePoolPage 的结构知道:

  • AutoreleasePool 并没有单独的结构,而是由若干个 AutoreleasePoolPage 以双向链表的形式组合而成(分别对应结构中的 parent 指针和 child 指针)。

下面我们先讲解几个在 AutoreleasePoolPage 中常用到的方法和变量。

TLS

Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以 key-value 的形式进行读写。

具体的内容,可以去查阅详细的资料,这里就不展开了(其实是本人偷懒)。

EMPTY_POOL_PLACEHOLDER

1
2
3
4
5
6
static inline id* setEmptyPoolPlaceholder()
{
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}

当释放池为空时,在 TLS 中 key 值对应指向的值 EMPTY_POOL_PLACEHOLDER。

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 中根据 key 值找到当前最新的 AutoreleasePoolPage,如果 page 为 EMPTY_POOL_PLACEHOLDER,则直接返回 nil。这里的 fastCheck() 就是通过 magic 校验内存是否损坏。

hotPage() 其实就是指向最新生成的 AutoreleasePoolPage 的对象。

coldPage()

1
2
3
4
5
6
7
8
9
10
11
static inline AutoreleasePoolPage *coldPage() 
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}

我们知道 AutoreleasePoolPage 是双向链表的节点,这里找到最新的 page,然后循环找到父节点,返回第一个节点。

coldPage() 方法是根据 hotPage() 找到的最后一个 page。

objc_autoreleasePoolPush

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
static inline void *push() {
id *dest = autoreleaseFast(POOL_BOUNDARY);
assert(*dest == POOL_BOUNDARY);
return dest;
}

static inline id *autoreleaseFast(id obj)
{
// 找到 hotPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 如果当前page可插入,则直接将对象插入到当前page中
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

/*
当前 page 满了之后,执行下面方法,找到子 page。

1. 如果无子 page,新建一个,并将对象插入到其中,再将其设为 hotPage。
2. 如果有,则判断子 page 是否满,满则继续查找子 page,否则将对象插入并设置 page 为 hotPage。
*/

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);

do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}

id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());

bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}

// We are pushing an object or a non-placeholder'd pool.

// Install the first page.
AutoreleasePoolPage *page = newAutoreleasePoolPage(nil);
setHotPage(page);

// Push a boundary on behalf of thepreviously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}

// Push the requested object or pool.
return page->add(obj);
}

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

  • 当前 hotPage 存在且没有满时,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。
  • 当前 hotPage 存在且已满时,调用 autoreleaseFullPage 初始化一个新的 page,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。再将其设为 hotPage。
  • 当前 hotPage 不存在时,调用 autoreleaseNoPage 创建一个 hotPage,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。再将其设为 hotPage。

POOL_BOUNDARY

push() 方法调用 autoreleaseFast(POOL_BOUNDARY) 时传入的是一个 POOL_BOUNDARY 并非需要被管理的对象,它的定义如下:

1
#define POOL_BOUNDARY nil

在调用 autoreleaseFast(obj) 方法会返回指向 obj 指针的指针,它是一个 id * 类型, 也就是说,这个返回值关心的只是 obj 指针的地址,而不是 obj 值的地址,obj 指针的地址就是对应 AutoreleasePoolPage 对象内存中的某段区域。
再看一下上层调用:

1
2
3
4
__AtAutoreleasePool __autoreleasepool = objc_autoreleasePoolPush();
......
//释放自动释放池
objc_autoreleasePoolPop(__autoreleasepool)

pop 时会将这个 obj 指针的地址传入进去。pop 的逻辑是把 hotPage 里面装的对象依次移除并发送 release 消息(后面会详细分析),当前 page 移除完了,继续移除 parent 节> 点内的对象,以此反复,而移除对象操作何时停止就是到这个obj指针的地址。

所以,push 操作加入一个 POOL_BOUNDARY 实际上就是加一个边界,pop 操作时根据边界判断范围,这就是一个入栈与出栈的过程。

objc_autoreleasePoolPop

objc_autoreleasePoolPop(__autoreleasepool) 的 __autoreleasepool 参数是 objc_autoreleasePoolPush() 返回的,实际上就是 POOL_BOUNDARY 对应的在 AutoreleasePoolPage 中的地址。最终会调用到 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
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
static inline void pop(void *token) 
{
AutoreleasePoolPage *page;
id *stop;

if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
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;
}
// 拿到 token 边界对应的 page
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();

// pop 内部对象直到 stop 边界
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);
}
// 删除 page 时的策略选择
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;

assert(offset >= sizeof(AutoreleasePoolPage));

result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();

return result;
}

void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage

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

// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}

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

if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
  • page->releaseUntil(stop),对栈顶(page->next)到 stop 地址(POOL_BOUNDARY)之间的所有对象调用 objc_release(),进行引用计数减1。

  • 如果当前的 page 中存放的对象少于一半,则子 page 全部删除;如果当前当前 的page 存放的多于一半(意味着马上将要满),则保留一个子 page,节省创建新 page 的开销。


常见问题

Autorelease对象什么时候释放?

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

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  • 第二个 Observer 监视了两个事件:
    • BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
    • Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有。

子线程的autoreleasepool对象的管理?

线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。所以在我们创建子线程的时候,如果没有获取 runloop,那么也就没用通过 runloop 来创建 autoreleasepool,那么我们的 autorelease 对象是怎么管理的,会不会存在内存泄漏呢?

答案是否定的。

  • 当子线程有 autoreleasepool 的时候,autorelease对象通过其来管理,
  • 如果没有 autoreleasepool,会通过调用 autoreleaseNoPage 方法,将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!这部分我们可以看下runtime中NSObject.mm的部分,有相关代码。