autoreleasepool 实现分析

文章目录
  1. 1. AutoRelease 内部结构
    1. 1.1. AutoreleasePage 结构
    2. 1.2. AutoreleasePage 原理分析
      1. 1.2.1. Page::push
      2. 1.2.2. Page::pop(ctxt)
  2. 2. 如何使用 AutoReleasePool呢
    1. 2.1. 主线程的 AutoReleasePool
    2. 2.2. 子线程 AutoReleasePool
    3. 2.3. 什么对象自动加入到 autoreleasepool中

AutoRelease 内部结构

AutoReleasePool 是利用语言上的特性,程序内存自动回收问题的

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

@autoreleasepool 是什么?在命令行中使用 clang -rewrite-objc main.m 让编译器重新改写这个文件:

1
2
3
4
5
6
7
8
9
struct __AtAutoreleasePool {
void * atautoreleasepoolobj;
__AtAutoreleasePool() { // 构造函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {// 析构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
};
1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

没有 AutoreleasePool 相关的类,而在 objc 源码中搜 objc_autoreleasePoolPush

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

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

AutoreleasePage 结构

AutoreleasePage 是实现 AutoreleasePool 的主要类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AutoreleasePoolPage {
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
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
static size_t const COUNT = SIZE / sizeof(id);//一个page里要管理释放对象的个数

#define EMPTY_POOL_PLACEHOLDER ((id*)1) // 空池占位
#define POOL_BOUNDARY nil // 释放池边界

magic_t const magic; // 用来校验 AutoreleasePoolPage 的结构是否完整
id *next; // 存储要用 AutoRelease 释放的对象的指针,初始化时指向 begin()
pthread_t const thread; // 所属 thread
AutoreleasePoolPage * const parent; // 指向父结点,第一个结点的 parent 值为 nil
AutoreleasePoolPage *child; // 指向子结点,最后一个结点的 child 值为 nil
uint32_t const depth; // 代表深度,从 0 开始,往后递增 1
uint32_t hiwat; // (high water mark)数据容纳的一个上限
}

POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界

AutoreleasePage 原理分析

以上知道 AutoreleasePage 的结构,根据分析过程,思考为何如此设计

Page::push

一开始调用的就是 AutoreleasePage::push()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void *push() {
id *dest;
dest = autoreleaseFast(POOL_BOUNDARY); // POOL_BOUNDARY 是 nil的宏定义
return dest;
}

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

autoreleaseFast 插入新 oc 对象元素的时候分三种情况需要处理:

  • page未满,直接插入到当前page
  • page已满,创建一个新page并插入
  • page不存在,创建一个新page并插入

最终都会通过 add 方法把当前需要背 AutoRelease 的对象放到 page 中,next++

1
2
3
4
5
6
7
8
id *add(id obj){
assert(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}

push 的过程

hotPage 是当前使用的页
对于autoreleasePool 多层嵌套问题,每一次push 首先插入 POOL_BOUNDARY 作为哨兵,然后接着 hotPage 中的 next 指针一个个添加对象

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

if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}

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

page->releaseUntil(stop);

// memory: delete empty children
if (page->lessThanHalfFull()) {
page->child->kill();
}else if (page->child->child) {
page->child->child->kill();
}
}

token : pop到参数元素所在的地址,整体过程

  1. 判断是不是EMPTY_POOL_PLACEHOLDER(EMPTY_POOL_PLACEHOLDER是存储在TLS中的用来表示链表最上层没有元素的pool,这样就不用创建pool可以节约内存。TLS是什么以及具体实现这篇[文章](https://blog.csdn.net/cywosp/article/details/26469435)介绍的比较详细
    1. 如果当前 pool里面有数据,就把里面的数据清空;否者就把hotPage设置为nil。
    2. 接着调用page->releaseUntil(stop)给此参数之前的所有对象发送release释放内存,对象内存释放之后终点page之前的page都会变成空的
    3. 最后调用page->child->kill()回收这些空page资源。

pop 的过程

如何使用 AutoReleasePool呢

官网文档

  1. 使用 Cocoa 框架 创建的 thread 都会维护自己的 autorelease pool. 如果你用其他方式自己创建的 thread 需要添加 autorelease pool.
  2. 如果你的 thread 常驻的,会产生大量临时对象,那就需要使用 autorelease pool (like AppKit and UIKit do on the main thread)

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

1
2
3
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];

主线程的 AutoReleasePool

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

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

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

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。

子线程 AutoReleasePool

子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?

  1. 如果创建了 pool 产生的对象由 pool 管理
  2. 如果没有创建 pool 产生了 AutoRelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage,并调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏
  3. Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

什么对象自动加入到 autoreleasepool中

黑幕背后的Autorelease
深入理解RunLoop
does NSThread create autoreleasepool automatically now
黑幕背后的Autorelease