AutoreleasePool 理解
从一些问题开始
- 什么是
AutoreleasePool? 说明一下NSAutoreleasePool具体机制? - ARC 时代和 MRC 时代的
AutoreleasePool机制有什么区别? AutoreleasePool的实现机制?AutoreleasePool和 NSRunloop 有什么关系?AutoreleasePool和线程有什么关系?- 什么时候需要我们手动创建
AutoreleasePool?
什么是 AutoreleasePool ? 如何理解 NSAutoreleasePool?
NSAutoreleasePool 对象的官方说明是一个支持 Cocoa 引用计数式内存管理的一个对象。 当池子排掉的时候向池子内存储的对象发送 release 消息。
An object that supports Cocoa’s reference-counted memory management system. An autorelease pool stores objects that are sent a release message when the pool itself is drained.
具体机制说明: 在引用计数式的内存管理中,NSAutoreleasePool 对象包含了收到了 _autorelease 消息的对象,这些 autorelease 对象(我们称被标记了 __autorelease 的对象为 autorelease 对象)的生命周期被延长到了这个 NSAutoreleasePool drain 的时候。也可以这么说 autorelease 和 release 的区别仅仅是 autorelease 是延时释放(即等待 AutoreleasePool drain) 而 release 是立即释放。
感觉说到这儿,其实我们可以说 NSAutoreleasePool 就是一个帮助我们管理内存的一个工具。
其实不光是我们自己可以手动创建 NSAutoreleasePool 对象,系统也帮我们维护了一个 NSAutoreleasePool 对象,在 runloop 迭代中不断 Push 和 Pop,从而不会堆积过多的 autorelease 对象引起内存疯长。你可能会好奇,哪会有那么多 autorelease 对象?举个例子来看一下:
- (void)viewDidLoad {
[super viewDidLoad];
// str 其实是一个 autorelease 对象
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
reference = str;
}题外话:为啥 str 是一个 autorelease 对象呢? 这个就需要知道下内存管理的知识了,使用 alloc,new,copy和mutableCopy这些关键字生成的对象是自己持有,反之不是(参考 Memory Management Policy)。使用 stringWithFormat: 类方法生成的 str 没有持有它的对象,只能通过 autorelease 这种方式来延长它的生命周期。具体 autorelease 的时机是在 stringWithFormat 内部做的。
Cocoa 的 Framework 里大量生成了 autorelease 的对象,所以官方说明里 Cocoa 代码执行是预期在一个 autorelease 环境中。
ARC 时代和 MRC 时代的 AutoreleasePool 机制有什么区别?
没啥根本区别,只是写法稍有不同。看两个 ARC 和 MRC 时代 autorelease 的两个经典写法。
MRC 的 case:
NSAutoreleasePool *pool = [[NSAutorelease alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];ARC 的 case(注:其实 MRC 也可以这么写):
@autoreleasepool {
//_autorelease 为所有权修饰符。
id _autorelease obj = [[NSObject alloc] init];
}ARC 中的几点变化:
ARC中是不能使用autorelease方法,也不能使用NSAutoreleasePool类。ARC系统提供了@autoreleasepool块来替代NSAutoreleasePool对象的生成持有以及废弃的功能。通过将对象赋值给附加了
__autoreleaseing修饰符变量来替代调用autorelease方法。即id obj = [[NSObject alloc] init]; [obj autorelease];等价于
id _autorelease obj = [[NSObject alloc] init];
一般我们不会显式的去使用 __autorelease 修饰符,因为 ARC 下编译器帮我们做了一些工作,即编译器会检查方法是否以 alloc/new/copy/mutableCopy 开始,如果不是的话将返回的值对象注册到 autoreleasePool。
不需要显式地写 __autorelease 的几种场景
自动释放池随意生成对象,不需要显式地添加
autorelease。@autoreleasepool { //默认的 strong 修饰符会自动处理这种情况. id obj = [[NSObject alloc] init]; }函数返回值的场景
+ (NSArray *)array { id obj = [[NSMutableArray alloc] init]; return obj; }在 MRC 时代,obj 是需要被发送
autorelease方法的,ARC 时代不需要这么做,这个对象作为函数的返回值会自动被注册到autoreleasePool中访问
weak变量的时肯定会涉及到autoreleasePool因为
weak对对象是弱引用,对象随时会被释放,但是使用autoreleasePool会延时释放,保证weak访问过程中不会出现对象被释放这种状况。NSObject **obj其实就是NSObject *_autorelease * obj。 因为我们不持有通过引用返回的对象。这种情况只能是autorelease。
AutoreleasePool 的实现机制?
分析过程
对以下代码所在文件执行 clang -rewrite-objc xx.m 重写命令,可以看到 OC 对应的 C++ 的源码。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}转换后的 C++ 代码。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_1280f1_mi_0);
}
return 0;
}可以看到 @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();
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_1280f1_mi_0);
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}具体 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的实现在 runtime 源码 NSObject.mm中可以找到。
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}AutoreleasePoolPage 的介绍
这里涉及到了 AutoreleasePoolPage 这个数据结构,接下来就看下 AutoreleasePoolPage 这个数据结构是啥样的?AutoreleasePoolPage 是个 C++ 的类
class AutoreleasePoolPage {
magic_t const magic; //magic 用于对当前 AutoreleasePoolPage 完整性的校验
id *next; //当前 autoreleasePoolPage 最上层的对象的指针。
pthread_t const thread; //thread 保存了当前页所在的线程
AutoreleasePoolPage * const parent;//指向上一个 AutoreleasePoolPage 对象.
AutoreleasePoolPage *child; //指向下一个 AutoreleasePoolPage 对象.
uint32_t const depth;
uint32_t hiwat;
}关于 AutoreleasePoolPage 的说明
可以看到其实并没有一个整体的自动释放池对象,自动释放池是由一个双向链表构成。当一个
AutoreleasePoolPage的空间被占满之后继续创建新的AutoreleasePoolPage对象。//child 指向的是下一个 AutoreleasePoolPage 对象的指针 // 这个方法是当前 page 如果满的情况下创建新的 page. id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); .... return page->add(obj); } // 初始化 pool 的方法 在这个里面对 parent 和 child 进行了赋值. 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->child = this; } }每个
AutoreleasePoolPage对象都存储着当前的线程id参考上面的AutoreleasePoolPage的初始化方法。使用pthread_self()拿到当前的线程id然后保存到thread成员变量里。AutoreleasePoolPage的内存大小是 4096 个字节。是 80386 机器上的每页的字节数。//初始化 AutoreleasePoolPage 的方法,size 是个宏定义的 4096 static void * operator new(size_t size) { return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE); }AutoreleasePoolPage存储autorelease对象是通过自己内部的next指针去实现。从实现上可以看到AutoreleasePoolPage还是从低内存地址向高内存地址增长。id *add(id obj) { id *ret = next; // faster than `return next-1` because of aliasing *next++ = obj; return ret; }由此大致能得到
AutoreleasePoolPage的内存结构如图(来自 Sunny 大神博客)
autorelease 消息调用栈
了解了这个数据结构后看下 autorelease 消息的调用栈。
我们看下 AutoreleasePoolPage 中 autorelease 方法实现其实就是将 autorelease 对象存储到 AutoreleasePoolPage 的过程。下面是大致实现的代码
static inline id autorelease(id obj) {
...
id *dest __unused = autoreleaseFast(obj);
...
return obj;
}
//这个是将 obj 存入 AutoreleasePoolPage 的方法。
static inline id *autoreleaseFast(id obj) {
//hotPage 应该是去 TLS(线程本地存储) 中获取 AutoreleasePoolPage。
//如果是程序刚启动的话,这儿肯定拿到的空。
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// AutoreleasePoolPage 不满的时候直接往进加
return page->add(obj); //绝大多数情况我们走的都是这个分支。
} else if (page) {
// AutoreleasePoolPage 满了,则创建新的 page,将 obj 放到新的 page 里去.
return autoreleaseFullPage(obj, page);
} else {
// 创建新的 page.
return autoreleaseNoPage(obj);
}
}autorelease pop 消息
对应 push 的是 pop,pop 即为将存储到 AutoreleasePoolPage 的对象释放对应原型为
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}注意的是这里并没有直接传入对象,而是传入了一个 ctxt 的指针,根据内部实现来看,自动释放池根据 ctxt 拿到它当前所在的 AutoreleasePoolPage ,然后将 AutoreleasePoolPage 的 ctxt 的位置开始到到最新的 AutoreleasePoolPage 存储的 autorelease 对象全部释放。即我们可以理解为自动释放池代码块儿开始的时候会在 AutoreleasePoolPage 进行一个占位,然后将后续的 autorelease 对象都放到占位后,这样就能确定当前自动释放池块儿里的对象是从哪到哪,理解了这一点也就能理解 autorelease 的嵌套实现了。
static inline void pop(void *token) {
AutoreleasePoolPage *page; id *stop;
..
page = pageForPointer(token); //拿到 token 所在的 AutoreleasePoolPage
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop); //一直释放对象到 token 的位置.
}
//一直释放对象的函数
void releaseUntil(id *stop) {
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage(); //拿到当前的 page.
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
if (obj != POOL_BOUNDARY) {
objc_release(obj); //取出对象不断发送 relesse 消息..
}
}
setHotPage(this);
}AutoreleasePool 和 NSRunloop 有什么关系?
先来个实例看下 Runloop 是什么东西。建一个普通的 Single View App 工程。点击按钮然后在按钮点击事件里打印
- (void)btnPressed:(id)sender {
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//在这里打断点然后 po runloop 得到下面结果。(省略大部分无关内容)
}
(lldb) po runloop
common mode items = <CFBasicHash 0x604000249360 [0x110875bb0]>
1 : <CFRunLoopObserver 0x6040001370c0 [0x110875bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110a24276), ....
......
4 : <CFRunLoopObserver 0x604000136ee0 [0x110875bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x110a24276), ....注意看上面的 activities,它对应的定义是这里
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};可以确定 Autorelease 机制在 Runloop 进入和退出(和休眠前触发) CommonMode 的时候进行观察,当 Runloop 运行到指定的时机的时候回触发 _wrapRunLoopWithAutoreleasePoolHandler 回调方法。
_wrapRunLoopWithAutoreleasePoolHandler 这个方法的实现其实我们并不清楚,网上没有找到对应的实现,不过我们可以打下符号断点来看看有没有线索。果然应用刚启动就执行了这些方法。看左侧的调用栈确实是从 Observer 的回调执行过来的。下面两个是我们熟悉的 Pop 和 Push 操作,基本上可以确认,Autorelease 机制是在进入 Runloop 的时候就创建了一个新的 AutoreleasePoolPage。退出或者休眠的的时候回收 AutoreleasePoolPage。

AutoreleasePool 和线程有什么关系?
Cocoa 应用程序里的每个线程会自动维护一个释放池,就是通过上面 Runloop 的方式。但是如果没有 Runloop 呢?
之前看到有人问了一个问题:子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗? 答案是不会。
具体 demo 如下 参考
- (void)viewDidLoad {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[thread start];
}
-(void)test {
MyClass *my = [[[MyClass alloc] init] autorelease];
NSLog(@"%@",[my description]);
}最后的结果是 MyClass 实例被释放掉了。理论上来说子线程并没有 Runloop 也就没有自动释放池观察 Runloop 状态,也就不会自动去执行对应的 autorelease 的方法。根据引用计数来看的话,autorelease 方法和 AutoreleasePool 在一起才能发生作用,而目前又没有 AutoreleasePool,所以那是咋回事?
事实上即使没有 Runloop,线程和 AutoreleasePool 也能直接发生关系。向某个对象发送 autorelease 消息后,会自动创建 AutoreleasePoolPage。autorelease 消息的调用栈可以参考上面的说明。最终 TLS(线程本地存储)会存储 AutoreleasePoolPage 对象。大致代码如下:
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
tls_set_direct(key, (void *)page);这里具体实现比较复杂,而且根据是这种情况并不适用于主线程。可以看 StackOverflow 的相关回答。这里不具体贴了。
我个人觉得为了程序可读性还有稳定性,还是加上 @autoreleasepool 更妥。说稳定性是因为不能过度依赖于 runtime 的底层机制,万一 runtime 底层机制后续有变化可能会造成程序的异常。
什么时候需要我们手动创建 AutoreleasePool?
- 如果工程只是 Foundation-Only(命令行那种),而不是 Cocoa application。那需要手动创建自动释放池。
- 如果程序存活时间长,而且可能生成大量临时对象(比如循环里创建了一堆)那应该在合适地方(比如循环里)手动释放池,降低内存峰值(不用担心嵌套使用
AutoreleasePool的问题) - 你创建了一个新线程,需要创建自动释放池。这个跟我们上面一小节说的是略微冲突,但是在上面已经说过了,添加
AutoreleasePool是最佳实践。
参考地址
黑幕背后的 Autorelease自动释放池的前世今生 ---- 深入解析 autoreleasepool深入理解 RunLoopiOS 中 autorelease 的那些事儿Transitioning to ARC Release NotesNSAutoreleasePoolUsing Autorelease Pool BlocksiOS ARC 内存管理要点各个线程 Autorelease 对象的内存管理
