内存管理一直是学习 Objective-C 的重点和难点之一,在实际的软件开发工作中,经常会遇见由于内存原因而导致的crash。而autorelease pool在内存管理中有着举足轻重的作用,只有理解了 autorelease pool 的原理,我们才算是真正了解了 Objective-C 的内存管理机制。下面我会从以下几个方面给大家讲解:
NSAutoreleasePool是什么?
NSAutoreleasePool实际上是个对象引用计数自动处理器,在官方文档中被称为是一个类。Objective-C的对象(全部继承自NSObject),就是使用引用计数的方法来管理对象的存活,众所周知,当引用计数为0时,对象就被销毁了。操作非常简单,当对象被创建时,引用计数被设成1。可以给对象发送retain消息,让对象对自己的引用计数加1。而当对象接受到release消息时,对象就会对自己的引用计数进行减1,当引用计数到了0,对象就会调用自己的dealloc处理。当对象被加入到NSAutoreleasePool中,会对其对象retain一次,当NSAutoreleasePool结束时,会对其所有对象发送一次release消息。NSAutoreleasePool可以同时有多个,它的组织是个栈,总是存在一个栈顶pool,也就是当前pool,每创建一个pool,就往栈里压一个,改变当前pool为新建的pool,然后,每次给pool发送drain消息,就弹出栈顶的pool,改当前pool为栈里的下一个pool。
NSAutoreleasePool的实现原理是什么?
@autoreleasepool {} 在编译时 @autoreleasepool {} 被转换为一个__AtAutoreleasePool ,通常这个结构体会在初始化时调用 objc_autoreleasePoolPush()方法,在析构时调用 objc_autoreleasePoolPop () 方法。而这些方法都是对AutoreleasePoolPage的简单封。AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。我们使用的所以想深入理解AutoreleasePool必须首先了解AutoreleasePoolPage。一个空的 AutoreleasePoolPage 的内存结构如下图所示:
一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,通过parent和child指针连接成链表,后来的autorelease对象在新的page加入。下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL (哨兵),即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。
到这里大家可能有点疑问。没关系,我下面会为大家详细讲解,首先我来介绍几个概念:
POOL_SENTINEL(哨兵对象)
你可能想要知道 POOL_SENTINEL 到底是什么,还有它为什么在栈中。首先回答第一个问题: POOL_SENTINEL 只是 nil 的别名。
#define POOL_SENTINEL nil
在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
上面的 atautoreleasepoolobj 就是一个 POOL_SENTINEL。
push 操作
objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
因此,我们接下来看看 AutoreleasePoolPage 的 push 函数的作用和执行过程。一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址,在执行 pop 操作的时候作为函数的入参。
static inline void *push()
{
id *dest = autoreleaseFast(POOL_SENTINEL);
assert(*dest == POOL_SENTINEL);
return dest;
}
push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作。重点内容
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { return autoreleaseNoPage(obj); } }
autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。
3.autorelease 操作
通过 NSObject.mm 源文件,我们可以找到 -autorelease 方法的实现
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
通过查看 ((id)self)->rootAutorelease() 的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的 autorelease 函数。
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || *dest == obj);
return obj;
}
4.POP操作
同理,前面提到的 objc_autoreleasePoolPop(void *) 函数本质上也是调用的 AutoreleasePoolPage 的 pop 函数
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
// fixme rdar://9167170
if (!ctxt) return;
AutoreleasePoolPage::pop(ctxt);
}
pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,就是pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。
对照之前的图,其实整个过程可以总结为:每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),当需要release时,objc_autoreleasePoolPop(哨兵对象)作为入参,根据传入的哨兵对象地址找到哨兵对象所处的page。在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
此时,如果执行 pop(token1) 操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:
NSAutoreleasePool何时释放?
当别人问你NSAutoreleasePool何时释放?你回答“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
小实验。
- (void)viewDidLoad {
[super viewDidLoad];
// 场景 1
NSString *string = [NSString stringWithFormat:@"1234567890"];
self.string_weak = string;
//场景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"1234567890"];
// _string_weak = string;
// }
// NSLog(@"string: %@",_string_weak);// 场景 3
// NSString *string = nil;
// @autoreleasepool {
// string = [NSString stringWithFormat:@”1234567890”];
//
// _string_weak = string;
// }
NSLog(@”string: %@”,self.string_weak);}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@”string: %@”, self.string_weak);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@”string: %@”, self.string_weak);
}
思考得怎么样了?相信在你心中已经有答案了。那么让我们一起来看看 console 输出:
//情景1
2016-10-27 20:29:06.069 AutoreleasePoolTest[974:138797] string: 1234567890
2016-10-27 20:29:06.070 AutoreleasePoolTest[974:138797] string: 1234567890
2016-10-27 20:29:06.076 AutoreleasePoolTest[974:138797] string: (null)
//情景2
2016-10-27 20:31:58.836 AutoreleasePoolTest[1003:141350] string: (null)
2016-10-27 20:31:58.837 AutoreleasePoolTest[1003:141350] string: (null)
2016-10-27 20:31:58.844 AutoreleasePoolTest[1003:141350] string: (null)
//情景3
2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: 1234567890
2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: (null)
2016-10-27 20:33:21.703 AutoreleasePoolTest[1014:142465] string: (null)
是不是和你所想一样?我们一起来分析下为什么会得到这样的结果。
分析1:当使用 [NSString stringWithFormat:@"1234567890"]创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的主线程的 autoreleasepool 中,autoreleasepool和strong同时拥有这个对象,所以引用计数为2。(如果这里直接输出 _string_weak的引用计数为3,因为 weak 的 retainCount 管理是独立的)。当程序执行完 - (void)viewDidLoad 时,string 释放,字符串对象引用计数-1。当执行- (void)viewWillAppear方法时,字符串对象引用计数为1。主线程的runloop一次迭代并没有结束,字符引用计数大于0,所以此时输出仍然有字符。而执行- (void)viewDidAppear时。runloop结束,autoreleasepool释放。字符串对象引用计数为0;所以此时字符串为空。
情景2: 当使用 [NSString stringWithFormat:@"1234567890"]创建一个对象时,string和autoreleasepool块同时持有,引用计数为2,当出来代码块后,string和autoreleasepool都会被释放。引用计数-2为0.字符串被释放,所以下面的输出全为空。
情景3: 申请字符串后,同时被string局部变量和代码块共有,引用计数为2,当出了autoreleasepool代码块的作用域时,字符串引用计数-1;由于string作用域为- (void)viewDidLoad 。所以第一次输出时字符串引用计数为1;当出了- (void)viewDidLoad 引用计数-1为0;此时字符串被释放。接下来两次输出都会为空。
如何使用Autorelease Pool Blocks
苹果文档介绍:很多程序会创造大量的临时对象,这些对象一直占用内存block的结束。大多情况下临时对象会一直在内存中聚集,直到当前的一次runloop迭代结束这样对内存造成很大的负担。苹果用了一段代码举例说明
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
AutoreleasePool与runloop和线程的关系
根据苹果官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。子线程的runloop需要自己手动创建,如果子线程的runloop没有任何事件,runloop会马上退出。(曾经我在写异步网络请求时想利用runloop来暂停线程来执行回调,但是由于没有添加任何事件源,导致runloop马上结束。网络请求失败)如果在每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。我们上面提到的场景 1 中创建的 autoreleased 对象就是被系统添加到了这个自动创建的 autoreleasepool 中,并在这个 autoreleasepool 被 drain 时得到释放。另外,NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。
本人第一次写博客。以后还会坚持下去。如有错误,欢迎大家来讨论。
相关链接:
Objective-C Autorelease Pool 的实现原理
自动释放池的前世今生 —- 深入解析 Autoreleasepool
官网文档
ARC的原理详解