参考
自动释放池的前世今生 ---- 深入解析 Autoreleasepool
你真的懂iOS的autorelease吗?
@autoreleasepool-内存的分配与释放
Autorelease Pool是什么
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
新建一个工程,总会见到这样的几行代码,这行代码将所有的事件、消息全部交给了UIApplication
来处理。
同时也说明,整个iOS应用,是包含在一个自动释放池block中的。
编译的时候,这段代码会被转换成
{
__AtAutoreleasePool __autoreleasepool;
}
其中,出现的结构体__AtAutoreleasePool
为
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这表明,我们的main
函数实际执行了
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
结论 @autoreleasepool
只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。
Autorelease Pool的主要结构
每一个autorelease pool
都是由一系列的AutoreleasePoolPage
组成
class AutoreleasePoolPage {
magic_t const magic; //完整性检验
id *next;
pthread_t const thread; //保存当前的进程
AutoreleasePoolPage * const parent; //双线链表使用 指向父节点
AutoreleasePoolPage *child; //双向链表使用 指向子结点
uint32_t const depth;
uint32_t hiwat;
};
可见,自动释放池的AutoreleasePoolPage
是以双向链表的结构连接起来的
而在自动释放池的内存中,AutoreleasePoolPage
被以栈结构存储起来,如下图
更多详细实现,请移步自动释放池的前世今生 ---- 深入解析 Autoreleasepool
MRC与ARC时代的Autorelease
MRC
(Mannul Reference Counting)和ARC
(Automatic Reference Counting),分别对应着手动引用计数和自动引用计数。
对!是计数,不是“ GC、垃圾回收 ”什么的,就是说,在Objective-C
的开发中,ARC
不代表像Java
那样有GC
做垃圾回收,所以本质上还是要“手动”管理内存的。也就是说,我们在ARC
环境下写的代码,不用自己手动插入“ retain
、release
这些消息 ”,ARC
会在编译时为我们在合适的位置插入,释放不必要的内存。
而 @autoreleasepool
就跟对象的release
密切相关。
在MRC
时代,如果我们想先retain
一个对象,但是并不知道在什么时候可以release
它,我们可以像下面这么做:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"tutuge"] autorelease];
//use str...
[pool release];
//str is released
就是说,我们可以在创建对象的时候给对象发送“ autorelease
”消息,然后当 NSAutoreleasePool
结束的时候,“标记过”autorelease
的对象都会被“ release
”掉,也就是会被释放掉。
但是在ARC
时代,我们不用手动发送autorelease
消息,ARC
会自动帮我们加。而这个时候,@autoreleasepool
做的事情,跟 NSAutoreleasePool
就一模一样了。
Autorelease对象的释放时机
实践内容可以参考 你真的懂iOS的autorelease吗?
在上面引用的文章中,笔者用__weak id
指针指向一个autorelease
对象,在不增加引用的情况下观察autorelease
对象的释放情况。
- 首先在
viewDidLoad
方法中初始化一个Array对象 - 分别尝试在
viewWillAppear
和viewDidAppeare
方法中观测Array
的值
发现,在viewWillAppear
方法中,对象未被释放,而到了viewDidAppear
中就被释放了,发现Array
对象并非超出作用域就马上被释放。得出结论,autorelease
并不是根据对象的作用域来决定释放时机。
实际上,autorelease
释放对象的依据是Runloop
,简单说,runloop
就是iOS
中的消息循环机制,当一个runloop
结束时系统才会一次性清理掉被autorelease
处理过的对象,其实本质上说是在本次runloop
迭代结束时清理掉被本次迭代期间被放到autorelease pool
中的对象的。至于何时runloop
结束并没有固定的duration。
扩展
既然由runloop
来决定对象释放时机而不是作用域,那么,在一个{}内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease
的释放了。
上文有提到autorelease pool
,一旦一个对象被autorelease
,则该对象会被放到iOS的一个池:autorelease pool
,其实这个pool
本质上是一个stack
,扔到pool
中的对象等价于入栈。我们把需要及时释放掉的代码块放入我们生成的autorelease pool
中,结束后清空这个自定义的pool
,主动地让pool
清空掉,从而达到及时释放内存的目的。以上述图片处理的例子为例,优化如下:
for (int i = 0; i <= 1000; i ++) {
//创建一个自动释放池
NSAutoreleasePool *pool = [NSAutoreleasePool new];//也可以使用@autoreleasePool{domeSomething}的方式
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
UIImage *image2 = [image imageByScalingAndCroppingForSize:CGSizeMake(480, 320)];
[image release];
//将自动释放池内存释放,它会同时释放掉上面代码中产生的临时变量image2
[pool drain];
}
这样,每次循环结束时,可以及时的释放临时对象的内存,其中,对自动释放池的操作可以用上文提到的方法来替代。
@autoreleasePool{
//domeSomeThing;
}
什么时候用@autoreleasepool
根据 Apple的文档 ,使用场景如下:
- 写基于命令行的的程序时,就是没有UI框架,如
AppKit
等Cocoa
框架时。 - 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
- 创建了新的线程。(非
Cocoa
程序创建线程时才需要) - 长时间在后台运行的任务。
什么对象会加入Autoreleasepool中
- 使用
alloc
、new
、copy
、mutableCopy
的方法进行初始化时,由系统管理对象,在适当的位置release
。 - 使用
array
会自动将返回值的对象注册到Autoreleasepool
。 -
__weak
修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool
中。 - id的指针或对象的指针,在没有显示指定时会被注册到
Autoleasepool
中。