autoreleasepool与runloop,线程(主线程,子线程)之间的关系。
前言
本文主要讲述autoreleasepool,其中runloop与线程的关系不作为重点(可参考的资料很多,当然本文也会稍提),想写demo测试验证的同学可以尝试在MRC下断点查看(在ARC下断点方法的调用栈有很多都被隐藏掉了),本文主要解决的问题如下:
- autoreleasepool的原理
- autoreleasepool在主线程的释放时机
- 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
11void *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 | /* Run Loop Observer Activities */ |
其中“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
在子线程的释放时机
问题分解:
- 子线程中是否存在
AutoreleasePool
? - 如果存在,那么里面的
autorelease
对象如何释放呢? - 如果不存在,如何建立一个
autoreleasepool
? - 自己建立的
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,在AutoReleaseModel
的dealloc
出加入断点,执行如下代码:
1 |
|
结果如下:@AutoreleasePool
的括号其实分别就是@NSAutoreleasePool
的初始化和drain
方法。当出了}
,就相当与调用drain
,从而调用AutoreleasePoolPage::pop
,向栈中的对象发送 release 消息
这里要补充一点 在ARC中,所有对象在alloc
,init
之后在编译后,都会被编译器加上autorelease
如下
1 | //原代码 |
而autorelease
的释放只能依赖@AutoreleasePool
,也就是说autorelease
对象会被加入@AutoreleasePool
,其实现如下
1 |
|
结论
- 在不自己建立
@AutoreleasePool
的情况下,主线程是通过监听CFRunLoopObserver
来决定释放autorelease
对象的时机的,而子线程则是在退出线程的时候来释放autorelease
对象。 - 如果自己手动建立
@AutoreleasePool
,那么autorelease
对象的声明周期仅在block块内,不论在主线程还是子线程都是如此。