李峰峰博客

Autorelease 实现原理

2020-08-30

1、@autoreleasepool{}

在新建 iOS 项目的时候,会自动生成 main.m 文件:

1
2
3
4
5
6
7
8
9
10
11
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

通过如下命令:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m  -o main.cpp

将其转成 C/C++ 代码,main 函数实现如下:

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

为了看起来方便,先去掉无关逻辑:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//......
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

可以看到,这里将 @autoreleasepool {} 转换成:

1
2
3
{ 
__AtAutoreleasePool __autoreleasepool;
}

继续在 main.cpp 中查找 __AtAutoreleasePool 的定义,可以找到:

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

可以确定 __AtAutoreleasePool 是一个结构体,这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,在析构时调用 objc_autoreleasePoolPop 方法。

也就是说,main 函数实际实现可以理解为:

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

// @autoreleasepool{} 的 {} 中的代码,会自动将创建的对象加入自动释放池

objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

在 OC 开源的源码中可以找到 objc_autoreleasePoolPushobjc_autoreleasePoolPop 实现如下(为了阅读方便,后续源码会有部分删简):

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

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

这两个方法是 AutoreleasePoolPage 对应静态方法 pushpop 的封装,其具体作用后文分析。

2、AutoreleasePoolPage

AutoreleasePool 并没有单独的结构,每一个 AutoreleasePool 都是由若干个 AutoreleasePoolPage 以双向链表的形式组成,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节,AutoreleasePoolPage 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AutoreleasePoolPage : private AutoreleasePoolPageData
{...}

struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
  • magic
    检查校验完整性的变量

  • next
    指向栈顶最新 add 进来的 autorelease 对象的下一个位置

  • thread
    page 当前所在的线程,AutoreleasePool 是按线程一一对应的(结构中的 thread 指针指向当前线程)

  • parent
    父节点,指向前一个 page

  • child
    子节点,指向下一个 page

  • depth
    链表的深度,节点个数

  • hiwat
    high water mark 数据容纳的一个上限

AutoreleasePoolPage 内存结构如下图:

每一个 AutoreleasePoolPage 的大小都是 4096 bit,其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的用来存储加入到自动释放池中的对象。

begin()end() 这两个类的实例方法用于快速获取 4096 bit - 56 bit = 4040 bit 这一内存范围的边界地址。

next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个对象,它就会如下图所示移动到下一个为空的内存地址中:

其中 POOL_BOUNDARY 是一个边界对象 nil(老版本变量名是 POOL_SENTINEL 哨兵对象)用来区别每个 AutoreleasePoolPage 边界,起到一个标识作用:

1
#define POOL_BOUNDARY nil

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

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, const char * argv[]) {
{
// 这里的 atautoreleasepoolobj 实际上就是 POOL_BOUNDARY
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// @autoreleasepool{} 的 {} 中的代码

objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

而调用 objc_autoreleasePoolPop 时,就会向自动释放池中的对象发送 release 消息,并在 AutoreleasePoolPage 中移除对应位置对象,直到第一个 POOL_BOUNDARY

3、push & pop

根据前面对 main.cpp 分析可知,@autoreleasepool{} 实际上就是在 {} 中的代码前后分别调用 objc_autoreleasePoolPush()objc_autoreleasePoolPop(atautoreleasepoolobj);

objc_autoreleasePoolPush() 实现如下:

1
2
3
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}

其内部调用的 AutoreleasePoolPage 中的 push 函数:

1
2
3
4
5
6
7
8
9
10
11
12
static inline void *push() 
{
id *dest;
if (slowpath(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;
}

在这里会调用 autoreleaseFast 函数,并传入边界对象 POOL_BOUNDARY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// a、有 hotPage 并且当前 page 不满
return page->add(obj);
} else if (page) {
// b、有 hotPage 并且当前 page 已满
return autoreleaseFullPage(obj, page);
} else {
// c、无 hotPage
return autoreleaseNoPage(obj);
}
}

a、将对象添加到自动释放池页中:

1
2
3
4
5
6
id *add(id obj)
{
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
return ret;
}

add 函数其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针。

b、有 hotPage 并且当前 page 已满时:

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

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

该函数会从传入的 page 开始遍历整个双向链表
如果找到未满的 AutoreleasePoolPage,则调用 add 函数添加对象。
如果没有找到未满的 AutoreleasePoolPage,则创建新的 AutoreleasePoolPage,并调用 add 函数添加对象。

最后将找到的或者新建的 AutoreleasePoolPage 标记为 hotPage.

c、无 hotPage:

1
2
3
4
5
6
7
8
9
10
11
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

if (obj != POOL_BOUNDARY) {
page->add(POOL_BOUNDARY);
}

return page->add(obj);
}

新建一个 AutoreleasePoolPage ,然后再加入 obj ,创建 Autorelease Pool 的时候,obj 的值是 POOL_BOUNDARY

我们用一张图来表示 Autorelease Pool 刚创建时候的结构:

总结上面 push 过程:
首先获取 hotPage,即当前正在使用的 AutoreleasePoolPage,然后针对 hotPage 的情况做不同处理:

  • 存在 hotPage & page 不满
    • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。
  • 存在 hotPage & page 已满
    • 调用 autoreleaseFullPage 函数,从传入的 page 开始往后遍历双向链表。
    • 如果找到未满的 AutoreleasePoolPage,则将该 page 标记为 hotPage
    • 如果没有找到未满的 AutoreleasePoolPage,则创建新的 AutoreleasePoolPage,并将该 page 标记为 hotPage
    • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。
  • hotPage
    • 调用 autoreleaseNoPage 函数创建一个 hotPage
    • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。

前边提到,销毁 Autorelease Pool 会调用 objc_autoreleasePoolPop 方法:

1
2
3
4
void objc_autoreleasePoolPop(void *ctxt) 
{
AutoreleasePoolPage::pop(ctxt);
}

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

page = pageForPointer(token);
stop = (id *)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 full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

根据前面的了解可知,这里 token 即创建 Autorelease Pool 时返回的 POOL_BOUNDARY,这个会作为 pageForPointer 的输入参数。 pageForPointer 函数的实现如下:

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

这里是为了计算出创建 Autorelease PoolAutoreleasePoolPage 的内存起始地址。所以,pageForPointer 函数返回的是当前 Autorelease Pool 创建时候的 AutoreleasePoolPage,获取到 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
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);
}

这里主要是从当前的 hotPage 开始,依次对 AutoreleasePoolPage 里的对象执行 objc_release 操作,直到遇到 POOL_BOUNDARY 对象。

总结上面 pop 流程:
首先,找到传入的 POOL_BOUNDARY 所在的 page,从 hotPage 开始(从自动释放池的中的最后一个入栈的 autorelease 对象开始),一直往前释放加入自动释放池的 autorelease 对象,可以向前跨越若干个 page,直到遇到这个 POOL_BOUNDARY,理的方式是向这些对象发送一次 release 消息,使其引用计数减一;

另外,清空 page 对象还会遵循一些原则:

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

接下来用图对 pushpop 过程进行一个演示:
首先,假设一个 AutoreleasePoolPage 中只能存储 4 个 Autorelease 对象,第一次创建 Autorelease Pool 时,有 5 个 Autorelease 对象需要放到缓存池中,这时候,第 5 个 Autorelease 对象只能存到一个新的 autoreleasePoolPage 中:

在上个 Autorelase Pool 还未销毁时,这时又新建了一个 Autorelase Pool,需要存储 2 个 Autorelease 对象,则往 AutorelasePoolPagenext 位置加入 POOL_BOUNDARY。并添加 obj6obj7

释放时,根据 POOL_BOUNDARY 找到所在 page,在对应 page 中,将晚于 POOL_BOUNDARY 插入的所有 autorelease 对象都发送一次 release 消息,并向回移动 next 指针到正确位置:

4、RunLoop 与 @autoreleasepool:

默认情况下,Autorelease 对象的释放时机是由 RunLoop 控制的,会在当前 RunLoop 每次循环期间时释放。
iOS 在主线程的 RunLoop 中注册了两个 Observer。

  • 第 1 个 Observer
    监听了 kCFRunLoopEntry 事件,会调用 objc_autoreleasePoolPush()

  • 第 2 个 Observer
    监听了 kCFRunLoopBeforeWaiting 事件,会调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    监听了 kCFRunLoopBeforeExit 事件,会调用 objc_autoreleasePoolPop()

所以,释放时机在 RunLoop 的如下三个事件中:

  • kCFRunLoopEntry
    在即将进入 RunLoop 时,会自动创建一个 __AtAutoreleasePool 结构体对象,并调用 objc_autoreleasePoolPush() 函数。

  • kCFRunLoopBeforeWaiting
    在 RunLoop 即将休眠时,会自动销毁一个 __AtAutoreleasePool 对象,调用 objc_autoreleasePoolPop()。然后创建一个新的 __AtAutoreleasePool 对象,并调用 objc_autoreleasePoolPush()

  • kCFRunLoopBeforeExit
    在即将退出 RunLoop 时,会自动销毁最后一个创建的 __AtAutoreleasePool 对象,并调用 objc_autoreleasePoolPop()