Objective-C autorelease

Objective-C 内存管理中的一个重要概念是 autorelease,自动释放。它类似于C语言中的自动变量的特性。回忆一下C语言中的自动变量:程序执行时,若某个自动变量超出其作用域,该自动变量将被自动废弃,不可再访问。autorelease 会像C语言的自动变量那样来对待对象实例,当超出变量作用域时,对象实例的 release 方法被调用。

autorelease 的具体使用方法如下:

  1. 生成并持有 NSAutoreleasePool 对象
  2. 调用对象实例的 autorelease 方法
  3. 废弃 NSAutoreleasePool 对象

NSAutoreleasePool 对象的生命周期相当于C语言变量的作用域。在废弃 NSAutoreleasePool 对象时,所有调用过 autorelease 的实例对象都将调用 release 方法。如下图所示:

使用代码表示如下:

1
2
3
4
5
6
7
//非ARC下
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
..
..
[pool drain];

最后一行的 [pool drain]等同于 [obj release]

NSAutoreleasePool 是与线程紧密相关的,每个线程都会维护一个自己的 NSAutoreleasePool 栈。在主线程 NSRunLoop 的每个 Loop 开始前,系统会自动创建一个 NSAutoreleasePool 对象 ,并在这个 Loop 结束时 drain 。所以在通常情况下,开发者不需要非得自己使用 NSAutoreleasePool 来进行开发工作。

自动释放池提供了一种机制,通过这种机制,你可以放弃对象的所有权,但又能避免对象被立即释放的可能性。通常你不需要创建自己的 autorelease pool ,但是某些场景下你必须这么做。

About Autorelease Pool Blocks

在 ARC 下,autorelease pool 使用关键字 @autoreleasepool 标记

1
2
3
@autoreleasepool {
// Code that creates autoreleased objects.
}

像其他代码一样,autorelease pool 也是可以嵌套的:

1
2
3
4
5
6
7
@autoreleasepool {
// . . .
@autoreleasepool {
// . . .
}
. . .
}

Cocoa 总是希望代码能够在 autorelease pool 中执行,否则 autorelease 对象将不会被释放,应用程序内存泄露。如果你在 autorelease pool block 之外向对象发送 autorelease 消息,Cocoa 会打印一条错误信息。AppKit 与 UIKit 框架会在 autorelease pool 中处理每个 event-loop 循环(比如鼠标按下事件或者一次点击),因此你通常不需要自己创建一个 autorelease pool,甚至很难见到这样的代码。然而,以下三种情况需要创建你自己的 autorelease pool:

  1. 你写的程序不是基于 UI framework 的,比如命令行工具
  2. 在一个循环中,你创建了大量临时对象. 你可以在循环内使用 autorelease pool 在下一次迭代之前处理这些对象,在循环中使用 autorelease pool 有助于减少应用程序的最大内存占用量
  3. Cocoa应用中的每个线程都维护自己的自动释放池堆栈。 如果您正在编写仅基于 Foundation 的程序或者分离线程,则需要创建自己的autorelease pool

使用 autorelease pool 来减少峰值内存占用量

许多应用创建的临时对象都是 autorelease 的,这些对象会持续添加到应用内存中,直到代码块结束。在大多数情况下,允许临时对象累积直到当前事件循环迭代结束不会导致过多的内存开销。

但是在少数情况下,你可能会创建大量临时对象,这些对象会大大增加内存占用,所以你希望快速的处理这些对象。此时你需要创建自己的 autorelease pool 。下面示例怎样在一个 for 循环中使用 autorelease pool

1
2
3
4
5
6
7
8
9
10
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. */
}
}

上述 for 循环每次处理一个文件,在 autorelease pool block 中的任何对象(比如 fileContents)会发送一个 autorelease 消息,在代码块结束时对象被 release。

在 autorelease pool block 结尾之后,应当认为上面所有的 autorelease 对象都已被处理。不要再向该对象发送消息或将其返回给方法调用者。如果你必须在 autorelease pool block 之外使用临时对象,可以在代码块内发送 retain 消息,然后在代码块结束之后向该对象发送 autorelease 消息,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
– (id)findMatchingObject:(id)anObject {

id match;
while (match == nil) {
@autoreleasepool {

/* Do a search that creates a lot of temporary objects. */
match = [self expensiveSearchForObject:anObject];

if (match != nil) {
[match retain]; /* Keep match around. */
}
}
}

return [match autorelease]; /* Let match go and return it. */
}

其他涉及到 autoreleasepool 的地方

以下是创建非自己生成、非自己持有的对象时,调用的类方法的源码示例:

1
2
3
4
+ (id)array
{
return [[NSMutableArray alloc] init];
}

上述代码可以写成如下形式:

1
2
3
4
5
6
7
8
9
10
11
+ (id)array
{
/*
* 自己生成并自己持有对象
*/
id obj = [[NSMutableArray alloc] init];
return obj;
/*
* 变量 obj 超出其作用域,编译器将它自动注册到 autoreleasepool
*/
}

没有显式制定所有权修饰符,所以 obj 默认是 __strong 的。由于 return 使得 obj 对象超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但是,该对象作为函数的返回值,编译器会自动将其注册到 autoreleasepool 。这也就是为什么 [NSMutableArray array] 会生成非自己生成、非自己持有的对象。

我们知道, weak 修饰符是为了避免循环引用而使用的,但是在访问附有 weak 修饰符的变量时,实际上必定要访问注册到 autoreleasepool 中的对象。比如有如下源码:

1
2
3
id obj0 = [[NSArray alloc] init];
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);

上述代码等价于:

1
2
3
4
id obj0 = [[NSArray alloc] init];
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

为什么在访问附有 weak 修饰符的变量时必须访问注册到 autoreleasepool 的对象呢?这是因为, weak 修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃。