文章目录
  1. 1. 关于AutoreleasePool测试
  2. 2. AutoreleasePool 源码解析
    1. 2.1. push操作
    2. 2.2. autorelease操作
    3. 2.3. pop操作
    4. 2.4. 整体结构
  3. 3. @autoreleasePool实现
  4. 4. Autoreleasepool和RunLoop
    1. 4.1. Thread、RunLoop和AutoreleasePool的关系
    2. 4.2. 主线程autoreleasepool的创建和释放机制
    3. 4.3. 对开头测试代码的解释
  5. 5. 参考

关于AutoreleasePool测试

我们先使用下面的测试代码来观察autorelease对象的释放时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__weak id  fd_object_weak = nil;

- (void)viewDidLoad {
[super viewDidLoad];

self.title = @"foundation框架";

NSString *string = [NSString stringWithFormat:@"auto release test-1"];
fd_object_weak = string;

NSLog(@"viewDidLoad : fd_object_weak = %@",fd_object_weak);
}

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"viewWillAppear : fd_object_weak = %@",fd_object_weak);
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"viewDidAppear : fd_object_weak = %@",fd_object_weak);
}

首先通过stringWithFormat创建一个autoreleased对象,将它赋值于一个全局的weak变量fd_object_weak,weak变量并不会影响所创建的autoreleased释放。这样我们就可以通过fd_object_weak变量来观察autoreleased对象的生命周期了。

运行后的打印结果是:

FDItemsTableViewController.m:45 viewDidLoad : fd_object_weak = auto release test-1

FDItemsTableViewController.m:51 viewWillAppear : fd_object_weak = auto release test-1

FDItemsTableViewController.m:57 viewDidAppear : fd_object_weak = (null)

当使用stringWithFormat创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的 autoreleasepool 中。当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。因为在 ARC 下 NSString string 本质上就是 __strong NSString string 。当 viewDidLoad 方法返回时,局部变量 string 被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1。

通过以上代码并不能确定autoreleased对象具体的释放时机,只是发现viewWillAppear中autoreleased对象未被释放在viewDidAppear中对象被释放了。由于weak 变量所指向的对象被释放时weak 变量的值会被置为 nil,设置观察点来观察fd_object_weak值的变化,在下面注释处设置断点

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];

self.title = @"foundation框架";

NSString *string = [NSString stringWithFormat:@"auto release test-1"];
fd_object_weak = string;

NSLog(@"viewDidLoad : fd_object_weak = %@",fd_object_weak);
//断点
}

进入断点后在lldb中输入设置断点命令watchpoint set v fd_object_weak然后继续,可以观察到测试程序停在如下位置

fd_object_weak观察

观察左侧的线程堆栈,看到了Runloop回调的方法:

1
2
_afterCACommitHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

这是一个线程即将进入休眠(BeforeWaiting)或线程退出的调用(exit)的调用。然后执行了方法 AutoreleasePoolPage::pop(void *)对 autoreleasepool 中的 autoreleased 对象执行 release 操作。

AutoreleasePool 源码解析

要了解 AutoreleasePool结构我可以阅读objc4开源代码。在NSObject.mm中可以找到autoreleasepool的实现说明和代码,下面是关于Autorelease Pool的整体说明

1
2
3
4
5
6
7
8
9
10
11
12
13
/***********************************************************************
Autorelease pool implementation

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.
**********************************************************************/

可以看出autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。AutoreleasePoolPage是一个C++类它以栈的形式存储autorelease对象指针和POOL_BOUNDARY,下面是简要实现代码(去除不相干部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
class AutoreleasePoolPage 
{
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.

magic_t const magic;
id *next; //栈顶指针
pthread_t const thread; //所属线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
}

下面分别分析AutoreleasePoolPage中的重要操作函数

push操作

NSObject中的objc_autoreleasePoolPush

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

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
35
36
37
38
public:
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;
}

//...
//找到hotPage
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);
}
}

//压栈操作
id *add(id obj)
{

assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}

为方便理解 add 函数中的*next++ = obj可以重写成下面语句

1
2
*next = objc;
next++;

objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。

一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token ,在执行 pop 操作的时候作为函数的入参。

push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作。分别对三种情况进行处理:

  • 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
  • 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
  • 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。

autorelease操作

NSObject 中的autorelease

1
2
3
4
// Replaced by ObjectAlloc
- (id)autorelease {
return ((id)self)->rootAutorelease();
}

objc_object类中的rootAutorelease

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

return rootAutorelease2();
}

. . .

__attribute__((noinline,used)) id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}

AutoreleasePoolPage类中的autorelease

1
2
3
4
5
6
7
8
static inline id autorelease(id obj)
{

assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

可以以看出autorelease操作和push操作都掉用了autoreleaseFast(),只不过push的参数是POOL_BOUNDARY,autorelease的参数是一个具体的对象。

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
void objc_autoreleasePoolPop(void *ctxt)
{

AutoreleasePoolPage::pop(ctxt);
}

//AutoreleasePoolPage::pop 代码很长,只列出关键逻辑
static inline void pop(void *token) {
AutoreleasePoolPage *page;
id *stop;

//找到pool token所在的AutoreleasePoolPage
page = pageForPointer(token);
stop = (id *)token;

//释放pool token之后的对象
page->releaseUntil(stop);

//释放链表中当前Page的子元素(page)
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 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。

整体结构

基于上面源码的分析下面用一张图来总结autoreleasepool的整体结构和操作

autoreleasepool

@autoreleasePool实现

通用以上源码我们就可以分@autoreleasepool关键字的实现了析首先我们通过clang rewrite来观察@autoreleasePool关键字的cpp代码

1
2
3
4
5
void hello(){
@autoreleasepool {

}
}

使用使用clang -rewrite-objc命令将上面的 Objective-C 代码重写得到下面c++代码

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

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

void hello(){
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

}
}

在代码块({})内创建的对象在代码块结束时会释放,所以上述hello方法内的代码可以为:

1
2
3
4
5
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(atautoreleasepoolobj);
}

objc_autoreleasePoolPush是创建一个自动释放池,objc_autoreleasePoolPop是释放一个自动释放池。

Autoreleasepool和RunLoop

现在我们还不能解释开头的实验,我们还需要知道Autoreleasepool和RunLoop的关系

Thread、RunLoop和AutoreleasePool的关系

下面是官方文档中的三个关键点

//1

Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

//2

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

//3

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

简单翻译下就是:

  1. 每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建
  2. 在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。
  3. 每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 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 了。

对开头测试代码的解释

两个从afterWaiting(线程被唤醒)到BeforeWaiting(准备进入休眠))我们暂且成为一个RunLoop循环,在一个RunLoop循环中线程会自动创建一个自动释放池,测试代码中stringWithFormat创建的对象就被放到了这个自动释放池中,这个RunLoop循环完成后自动释放池被释放对象也被释放,

为什么对象在viewWillAppear中未被释放而在viewDidAppear函数中被释放了呢?只能说明这两个函数不在同一个RunLoop循环中执行,而且由于对象在viewWillAppear中未被释放说明viewWillAppear和viewDidLoad在同一个RunLoop循环中执行。

参考

OpenSoure-objc4

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

黑幕背后的Autorelease

NSAutoreleasePool - API Reference