余额不足.

Autorelease知多少

字数统计: 2.4k阅读时长: 10 min
2019/11/05 Share

最近脑子里冒出一个疑问,子线程的RunLoop默认是没有开启的,需要程序猿手动去开启,那子线程的Autorelease对象是在什么时候被销毁的?

带着问题google了一翻,发现有些博客里说得也挺好的,但就是看不下去,我这颗浮躁的心啊!带着问题自己去溯源吧,可能看源码更能让我快乐一些!手动狗头。

从头开始

国际惯例是对含有AutoreleasePool的代码进行clang -rewrite,大家都明白怎么一回事,为了省点篇幅就省略了,直接标记重点。

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

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

通过AutoreleasePoolPage调用push()将对象塞入自动释放池,调用push()这个函数时都发生了哪些不为人知的事呢?

AutoreleasePoolPage

1
2
3
4
5
6
7
8
9
10
11
12
class AutoreleasePoolPage 
{
# define POOL_BOUNDARY nil // 哨兵对象
static size_t const SIZE = PAGE_MAX_SIZE // Page的大小,实际上值为4096
magic_t const magic; // 用于校验AutoreleasePoolPage的完整性
id *next; // 用于测量Page当前存储位置的指针
pthread_t const thread; // 当前page所在线程
AutoreleasePoolPage * const parent; // 当前page的父page
AutoreleasePoolPage *child; // 当前page的子page
uint32_t const depth;
uint32_t hiwat;
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}

析构函数

1
2
3
4
5
6
7
~AutoreleasePoolPage() 
{
check();
unprotect();
assert(empty());
assert(!child);
}

了解了AutoreleasePoolPage的基本构造,接下来就是他的工作原理。

Push()

按照正常的流程AutoreleasePoolPage::push()会直接调用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()) {
// 如果当前page未满,直接在page中添加obj
return page->add(obj);
} else if (page) {
// 如果page满了
return autoreleaseFullPage(obj, page);
} else {
// 如果page不存在,则新建page,并添加obj
return autoreleaseNoPage(obj);
}
}

先看看add()

1
2
3
4
5
6
7
8
9
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}

将需要添加的对象放在next指针的后面,保证next指针始终处于高地址,

add操作

如果Page满了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
// 如果当前page满了,先判断当前page下是否有子page
// 如果有子page,则将子page设置成当前page,并添加obj
// 如果没有子page,则新建一个page,并添加obj
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

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

到这,一个大致的添加流程就结束了。
然后就是pop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline void pop(void *token) 
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
···
page->releaseUntil(stop);
···
// 当前 page 使用不满一半,从 child page 开始将后面所有 page 删除
// 当前 page 使用超过一半,从 child page 的 child 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()内部调用releaseUntil(stop),遍历page链表释放对象直到找到哨兵对象为止。

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
void releaseUntil(id *stop) 
{
// 释放当前Page中的对象
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);
}

这里的原注释还挺有意思// fixme I think this 'while' can be 'if', but I can't prove it
这里的释放逻辑也比较简单,一直遍历释放,如果当前page已经空了,则设置父page为当前page继续遍历,直到找到哨兵对象。

如图所示,从Page2的ObjM开始释放,一直释放到哨兵对象为止,最终的结果就是

到这里自动释放池的释放流程就走完了。

主线程的AutoReleasePool

通过上文我们对AutoReleasePool的工作原理应该有了一定的认知,那么进入下一环节,纵贯APP生命周期的RunLoop能和AutoReleasePool碰撞出什么样的火花呢?

1
2
3
4
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"RunLoop: %@", [NSRunLoop mainRunLoop]);
return YES;
}

didFinishLaunchingWithOptions输出一下当前的主RunLoop

1
2
3
4
5
6
7
8
9
···
observers = (
"<CFRunLoopObserver 0x600001cc41e0 [0x7fff80615350]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848cac), context = <CFArray 0x600002382d30 [0x7fff80615350]>{type = mutable-small, count = 0, values = ()}}",
"<CFRunLoopObserver 0x600001cf0000 [0x7fff80615350]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff473eda92), context = <CFRunLoopObserver context 0x6000006cc310>}",
"<CFRunLoopObserver 0x600001cc40a0 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff478792c7), context = <CFRunLoopObserver context 0x7fde93100fe0>}",
"<CFRunLoopObserver 0x600001cc4140 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff47879330), context = <CFRunLoopObserver context 0x7fde93100fe0>}",
"<CFRunLoopObserver 0x600001cc4280 [0x7fff80615350]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848cac), context = <CFArray 0x600002382d30 [0x7fff80615350]>{type = mutable-small, count = 0, values = ()}}"
),
···

从上面的输出我们看见App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),这两个activities分别是0x10xa0,转换为10进制后然后对照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Run Loop Observer Activities */

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)

{

kCFRunLoopEntry = (1UL << 0), // 1 即将进入 Loop

kCFRunLoopBeforeTimers = (1UL << 1), // 2 即将处理 Timer

kCFRunLoopBeforeSources = (1UL << 2), // 4 即将处理 Source

kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即将进入休眠

kCFRunLoopAfterWaiting = (1UL << 6), // 64 刚从休眠中唤醒

kCFRunLoopExit = (1UL << 7), // 128 即将退出 Loop

kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有事件

};

我们可以得知0x1kCFRunLoopEntry0xa0kCFRunLoopBeforeWaiting||kCFRunLoopExit,即在这三种状态下都会触发_wrapRunLoopWithAutoreleasePoolHandler回调。但是_wrapRunLoopWithAutoreleasePoolHandler为私有函数,该函数内部都做了些什么操作我们没办法知道,但是我们可以通过符号断点分析他大概都做了些什么。

从这段汇编代码我们可以看到根据不同条件_wrapRunLoopWithAutoreleasePoolHandler会调用NSPushAutoreleasePoolNSPopAutoreleasePool,然后继续对这两个函数下符号断点,发现NSPushAutoreleasePool会调用objc_autoreleasePoolPush,NSPopAutoreleasePool则是调用objc_autoreleasePoolPop,然后参照上文的分析,是不是就融会贯通豁然开朗了?

APP启动时在即将进入RunLoop状态下(kCFRunLoopEntry)会调用objc_autoreleasePoolPush创建AutoreleasePool
在即将进入休眠状态下(kCFRunLoopBeforeWaiting)则是调用objc_autoreleasePoolPopobjc_autoreleasePoolPush 释放旧的池并创建新池。
即将退出 Loop(kCFRunLoopExit)调用objc_autoreleasePoolPop释放自动释放池。
所以在主线程执行的Source回调、Timer回调都会被RunLoop创建好的AutoreleasePool管理,通常情况下是不会出现内存泄漏的情况的。

关于子线程的AutoReleasePool

回到文首,当子线程的RunLoop没有开启时,AutoReleasePool又是怎样工作的?

NSThread

首先定义一个AString类,dealloc处下个断点,然后

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

[NSThread detachNewThreadSelector:@selector(athread) toTarget:self withObject:nil];
}

- (void)athread {
__autoreleasing AString *a = [AString new];
NSLog(@"%@", a);
}

run一下,直接进入断点,我们看看delloc的调用栈

能分析出大致的流程是线程退出时会调用AutoreleasePoolPagetls_dealloc(),然后释放自动释放池中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void tls_dealloc(void *p) 
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
// No objects or pool pages to clean up here.
return;
}

// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);

if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) pop(page->begin()); // pop all of the pools
if (DebugMissingPools || DebugPoolAllocation) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}

// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}

如果有AutoreleasePoolPage对象存在则会触发pop流程,然后将所有的AutoreleasePoolPage对象都给清空了。

GCD

上一小节介绍了NSThread开辟线程情况下的AutoreleasePool释放流程,那么GCD下会不会和NSThread是同样的调用流程?

依然还是在AStringdealloc()下断点

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

dispatch_queue_t aq = dispatch_queue_create("aaa", 0);
dispatch_async(aq, ^{
__autoreleasing AString *a = [AString new];
NSLog(@"%@", a);
});
}

可以发现GCD的调用栈和NSThread的调用栈的底层还是有区别的,并不是我们想象中会是一样的结果。
调用栈中可以发现_dispatch_last_resort_autorelease_pool_pop()这样一个我们想看见的函数

1
2
3
4
5
6
7
8
void
_dispatch_last_resort_autorelease_pool_pop(dispatch_invoke_context_t dic)
{
if (likely(!_os_object_debug_missing_pools)) {
_dispatch_autorelease_pool_pop(dic->dic_autorelease_pool);
dic->dic_autorelease_pool = NULL;
}
}

该函数的入参是个GCD上下文对象,然后调用_dispatch_autorelease_pool_pop(),并将gcd上线文对象中的autoreleasepool传进去。

从汇编代码中可以发现_dispatch_last_resort_autorelease_pool_pop()直接调用了objc_autoreleasePoolPop()释放自动释放池中的对象。

子线程如果一次性跑无限多的任务会不会爆栈?

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
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

Thread.detachNewThread {
for i in 0...1000000 {
autoreleasepool {
self.abc(i)
}
}
}
}

func abc(_ i: Int) {
if let path = Bundle.main.path(forResource: "content", ofType: "json") {
let url = URL(fileURLWithPath: path)
do {
let data = try! Data(contentsOf: url)
let jsonData:Any = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
print(jsonData)
print("--------------------" + "\(i)" + "------------------" )
print(Thread.current)
}
}
}
}

按照上文的理论,如果一次性在子线程添加1000000条任务,在tls_dealloc处下个符号断点,然后运行,然后被断点断住,通过日志可以发现只打印了177条数据,也就是说明当线程任务达到一定程度时编译器就不会再继续给该线程派发任务了,这时候清理一波autoreleasePool的内存,然后继续干没干完的工作。所以在子线程中放心的使用autoreleasePool吧,爆栈是不会发生的!

小结

NSThread开辟的子线程最终会调用tls_dealloc()去调用pop()释放自动释放池,而GCD则有API去维护自动释放池。

CATALOG
  1. 1.
  2. 2. 从头开始
    1. 2.1. AutoreleasePoolPage
      1. 2.1.1. Push()
  3. 3. 主线程的AutoReleasePool
  4. 4. 关于子线程的AutoReleasePool
    1. 4.1. NSThread
    2. 4.2. GCD
    3. 4.3. 子线程如果一次性跑无限多的任务会不会爆栈?
    4. 4.4. 小结