__autoreleasepool修饰符的底层实现分析

还是直接打开objc4的源码看是最直接的了,从苹果的开源网站下载objc4的源码:https://opensource.apple.com/tarballs/objc4/,我下载的是objc4-750这个版本。之后在工程中找到NSObject.mm这个文件,关于自动释放池的实现都在`AutoreleasePoolPage`这个类中了。在阅读的过程中看不懂的地方可以参考 draveness 大神的这篇文章:自动释放池的前世今生 —- 深入解析 autoreleasepool

这篇文章是我自己对这块学习做的总结,主要从这两方面入手:系统是如何来设计自动释放池的,以及autorelease对象的push和pop的过程。

Autoreleasepool的设计

从源码中我们可以看到实际上并没有Autoreleasepool这个类,只有AutoreleasePoolPage这个类就完成了自动释放池的实现。它的定义如下:

1
2
3
4
5
6
7
8
9
10
class AutoreleasePoolPage 
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}

magic用于对当前AutoreleasePoolPage完整性的校验。

next指针通常出现在单链表中,实际上可以把一个AutoreleasePoolPage看成一个链式栈。next指针指向栈中最新添加的autorelease对象的下一个地址空间。

一个AutoreleasePoolPage是不会一直增长下去的,它有一个固定的大小,这个大小是 4096 bytes。当一个page已经满了的时候,会去再重新创建一个page。

一个page的结构如下图所示:

thread指针指向当前线程,每个线程都有属于它的自动释放池。在与该线程相关的RunLoop处理完一次事件后会释放自动释放池中的对象。

此外,我们看到还有parentchild两个指针,这明显是双向链表的特点,一个AutoreleasePoolPage就是双向链表中的一个结点。这个双向链表就是一个自动释放池,如下图所示:

depthhiwat可以暂且不管,它们并不会影响整个自动释放池的实现。

其实,Autoreleasepool的push和pop的过程,也就是维护这个双向链表的过程。

Autoreleasepool的push和pop

1
2
3
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}

可以转换成如下代码:

1
2
3
4
5
id pool = objc_autoreleasePoolPush();
id obj = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

在代码中可能随时都会来上一段@autoreleasepool {},而且调用@autoreleasepool是可以嵌套的。调用objc_autoreleasePoolPush函数就开始了一段autoreleasepool,然后调用objc_autorelease函数像池子中添加要自动释放的对象,最后再调用objc_autoreleasePoolPop函数向在这期间添加的对象发送release消息。

这几个函数在源码中如下:

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
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

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

// AutoreleasePoolPage中的push函数
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;
}

// AutoreleasePoolPage中的pop函数,这里只摘出了关键代码
// 调用该方法时需要传入一个token,释放到token以及之后的对象
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;

page = pageForPointer(token);
stop = (id *)token;

page->releaseUntil(stop);

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

整个的pop过程如下图所示,push的过程反过来即可。

这其中的几点下面进一步说明。

hotPage

hotPage是当前正在使用的page,在push时对象是添加到hotPage中,pop时也是释放的hotPage中的对象。同时在push和pop时也会更新hotPage。

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

与之相对的还有一个coldPage,是双链表中最前面的那个page。

POOL_BOUNDARY

1
#   define POOL_BOUNDARY nil

一个线程只有一个上面说的双向链表,所以怎样才能知道一段自动释放池从哪里开始呢?所以这里用了POOL_BOUNDARY作为哨兵来标识。在AutoreleasePoolPage的push函数中先在page中放了一个POOL_BOUNDARY,然后又将POOL_BOUNDARY返回,最后将这个返回值传入到pop中。这样在释放时就知道要释放哪一段的对象了。

EMPTY_POOL_PLACEHOLDER

1
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

还有这样一个宏,它是用来标识目前还没有自动释放池,也就是还没有page呢。在push的时候如果当前没有page,就先不创建AutoreleasePoolPage,而是在TLS中存入EMPTY_POOL_PLACEHOLDER,并将其返回。这样在push和pop之间如果没有添加要自动释放的对象,就不会浪费内存。

autoreleaseFast

autorelease函数和push函数中都调用autoreleaseFast函数,它在AutoreleasePoolPage中的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

这段代码的逻辑:

  • 获取hotPage,也就是拿到当前正在使用的page。
  • 如果hotPage还没有放满,就直接添加到该page中。
  • 如果hotPage已经满了,调用autoreleaseFullPage函数。这个函数中先找hotPage的child是否存在,存在还没有满就找到了适合的page。如果没有找到就创建一个新的page:page = new AutoreleasePoolPage(page);,原来的hotPage成为了新page的parent。之后更新hotPage,添加对象到新page中。
  • 如果没有hotPage,调用autoreleaseNoPage函数。此时说双向链表还没有创建,所以在创建page时传入的是nil:page = new AutoreleasePoolPage(nil);,该page的parent为空。

pageForPointer

在pop时我们知道了有POOL_BOUNDARY作为标识,可知要释放对象一直到哪里。但是因为autoreleasepool可以嵌套,并且可以有多个,POOL_BOUNDARY肯定也会多次出现,那怎么知道要释放的是哪个page中的对象呢?这里并不能拿hotPage来用。所以用pageForPointer函数对地址进行运算,得到了page。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

将指针与page的大小,也就是4096取模,得到了当前指针的偏移量,然后再用指针减去偏移量就得到了page的首地址。

参考资料

【1】 《Objective-C高级编程》1.4节

【2】 黑幕背后的Autorelease

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

【4】 理解 ARC 实现原理

本文作者:意林
本文链接:http://shinancao.cn/2019/07/17/iOS-autoreleasepool/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!