从一些问题开始
- 什么是
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 (opens new window))。使用 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 也可以这么写 (opens new window)):
@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
。 因为我们不持有通过引用返回的对象 (opens new window)。这种情况只能是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 如下 参考 (opens new window)
- (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 (opens new window) 的相关回答。这里不具体贴了。
我个人觉得为了程序可读性还有稳定性,还是加上 @autoreleasepool
更妥。说稳定性是因为不能过度依赖于 runtime
的底层机制,万一 runtime
底层机制后续有变化可能会造成程序的异常。
# 什么时候需要我们手动创建 AutoreleasePool?
- 如果工程只是 Foundation-Only(命令行那种),而不是 Cocoa application。那需要手动创建自动释放池。
- 如果程序存活时间长,而且可能生成大量临时对象(比如循环里创建了一堆)那应该在合适地方(比如循环里)手动释放池,降低内存峰值(不用担心嵌套使用
AutoreleasePool
的问题) - 你创建了一个新线程,需要创建自动释放池。这个跟我们上面一小节说的是略微冲突,但是在上面已经说过了,添加
AutoreleasePool
是最佳实践。
# 参考地址
黑幕背后的 Autorelease (opens new window) 自动释放池的前世今生 ---- 深入解析 autoreleasepool (opens new window) 深入理解 RunLoop (opens new window) iOS 中 autorelease 的那些事儿 (opens new window) Transitioning to ARC Release Notes (opens new window) NSAutoreleasePool (opens new window) Using Autorelease Pool Blocks (opens new window) iOS ARC 内存管理要点 (opens new window) 各个线程 Autorelease 对象的内存管理 (opens new window)