autoreleasepool

autoreleasepool与runloop,线程(主线程,子线程)之间的关系。

前言

本文主要讲述autoreleasepool,其中runloop与线程的关系不作为重点(可参考的资料很多,当然本文也会稍提),想写demo测试验证的同学可以尝试在MRC下断点查看(在ARC下断点方法的调用栈有很多都被隐藏掉了),本文主要解决的问题如下:

  1. autoreleasepool的原理
  2. autoreleasepool在主线程的释放时机
  3. autoreleasepool在子线程的释放时机(子线程默认不开启runloop)

autoreleasepool的数据模型

先放一份autoreleasepool的源码地址。新建demo工程。clang main.m文件,打开main.cpp文件查看源码如下

1
2
3
4
5
6
7
8

int main(int argc, char * argv[]) {
//可以看见@autoreleasepool 被转换成了__AtAutoreleasePool。
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

可以看见@autoreleasepool 被转换成了__AtAutoreleasePool,在当前文件搜索出现下面内容。

1
2
3
4
5
6
7
8
9
10

struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};

上面是__AtAutoreleasePool结构体的组成,其中主要的两个方法 objc_autoreleasePoolPush()objc_autoreleasePoolPop(),这两个方法我们可以在NSObject.mm的源码中找到实现如下:

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

void objc_autoreleasePoolPop(void *ctxt) {
if (UseGC) return;
// fixme rdar://9167170
if (!ctxt) return;
AutoreleasePoolPage::pop(ctxt);
}

关键的来啦:通过上面的代码我们已经知道@autoreleasepool 的背后其实一直在默默工作的是AutoreleasePoolPage,因为在很多老的blog中没有体现出如何找到AutoreleasePoolPage,所以关于autoreleasepool的源码介绍就到这里,这里有一篇参考资料。这里面会讲述AutoreleasePoolPage的数据结构,及维护原理。

autoreleasepool在主线程的释放时机

在demo中加入新的断点。如果在主线程,直接执行lldb命令:po [NSRunLoop currentRunLoop] 如果在子线程则需要MainRunLoop。
断点结果
图片如果看不清的同学可以浏览器网页缩放下。
我们可以看到主线程的runLoop 中有两个Observer,并且 activities 分别为“0xa0”和“0x1”这是两个十六进制的数,转为二进制分别为“1”和“10100000”,对应CFRunLoopActivity的类型如下:

1
2
3
4
5
6
7
8
9
10
11
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry = (1UL << 0), //0x1
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5), //0xa0 = kCFRunLoopBeforeWaiting | kCFRunLoopExit
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7), //0xa0 = kCFRunLoopBeforeWaiting | kCFRunLoopExit
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

其中“0x1”即kCFRunLoopEntry,监听RunLoop对象进入循环的事件,“0xa0”即kCFRunLoopBeforeWaiting | kCFRunLoopExit,监听RunLoop即将进入休眠和RunLoop对象退出循环的事件。

程序运行后产生的两个CFRunLoopObserver一个监听RunLoop对象进入循环的事件,执行回调函数_wrapRunLoopWithAutoreleasePoolHandler,并且优先级order-2147483647即32位整数的最小值,保证了它的优先级最高。在回调内会调用_objc_autoreleasePoolPush函数来创建AutoreleasePool,由于它的优先级最高,所以能够保证自动释放池在其他回调执行前得到创建。

另一个监听器监听RunLoop对象进入休眠和退出循环的事件,回调函数同样是_wrapRunLoopWithAutoreleasePoolHandler,而优先级为2147483647即32位整数的最大值,保证它的优先级最低。对于监听进入休眠状态时回调函数内首先会调用_objc_autoreleasePoolPop函数来释放AutoreleasePool然后使用_objc_autoreleasePoolPush函数重新创建一个自动释放池。优先级最低保证了释放操作是在其他所有回调执行之后发生。

autoreleasepool在子线程的释放时机

问题分解:

  1. 子线程中是否存在AutoreleasePool
  2. 如果存在,那么里面的autorelease对象如何释放呢?
  3. 如果不存在,如何建立一个autoreleasepool
  4. 自己建立的autoreleasepool何时释放?

先来回答第一个问题,这里有一份苹果的开发文档这里明确的指出了

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks.

其实这篇文档基本是回答了上面的所有问题,子线程是存在AutoreleasePool的,在线程退出的时候会去做释放autorelease对象,这也证明了问什么在子线程的for循环中释放会不及时,因为for里面产生的autorelease对象是要等到线程退出的时候再去释放的。下面举个例子

1
2
3
4
5
6
7
8
9
10
11

dispatch_async(dispatch_get_global_queue(0, 0), ^{

for (int i = 0; i<1000000000; i++) {
//@autoreleasepool {
AutoReleaseModel *model = [[AutoReleaseModel alloc] init];
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
NSLog(@"str = %@", str);
//}
}
});

我们运行下面代码会发现,内存在逐步增加。
如图
当打开注释掉的@autoreleasepool时,内存如下
如图
所以子线程是可以建立自己的AutoreleasePool的,并且自己建立的autoreleasepool里的面对象在出了block就会释放,这里可以参考 NSAutoreleasePool,我们可打开MRC,在AutoReleaseModeldealloc出加入断点,执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

dispatch_async(dispatch_get_global_queue(0, 0), ^{

for (int i = 0; i<1000000000; i++) {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
NSLog(@"str = %@", str);
AutoReleaseModel *model = [[AutoReleaseModel alloc] init];
[model autorelease];
[pool drain];
}
});

结果如下:
如图
@AutoreleasePool的括号其实分别就是@NSAutoreleasePool 的初始化和drain方法。当出了},就相当与调用drain,从而调用AutoreleasePoolPage::pop,向栈中的对象发送 release 消息
这里要补充一点 在ARC中,所有对象在allocinit之后在编译后,都会被编译器加上autorelease如下

1
2
3
4
5
6
//原代码
AutoReleaseModel *model = [[AutoReleaseModel alloc] init];

//编译之后
//AutoReleaseModel *model = [[AutoReleaseModel alloc] init];
//[model autorelease]

autorelease的释放只能依赖@AutoreleasePool,也就是说autorelease对象会被加入@AutoreleasePool,其实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)

结论

  1. 在不自己建立@AutoreleasePool的情况下,主线程是通过监听CFRunLoopObserver来决定释放autorelease对象的时机的,而子线程则是在退出线程的时候来释放autorelease对象。
  2. 如果自己手动建立 @AutoreleasePool,那么autorelease对象的声明周期仅在block块内,不论在主线程还是子线程都是如此。