第34条:以“自动释放池块”降低内存峰值
本条要点:(作者总结)
- 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
Objective-C 对象的生命期取决于其引用计数(参见第29条)。在 Objective-C 的引用计数架构中,有一项特性叫做“自动释放池”(autorelease pool)。释放对象有两种方式:一种是调用 release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入 “自动释放池”中。自动释放池用于存放那些需要稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送 release 消息。
创建自动释放池所用语法如下:
1 @autorelease { 2 3 // ... 4 5 }
如果在没有创建自动释放池的情况下给对象发送autorelease 消息,那么控制台会输出这样一条消息:
Object 0xabcd0123 of class __NSCFString autoreleased
with no pool in place - just leaking -break on objc_ autoreleaseNoPool() to debug
然而,一般情况下无须担心自动释放池的创建问题。Mac OS X 与 iOS 应用程序分别运行于 Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是 “大中枢派发”(Grand Central Dispatch, GCD)机制中的线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。因此,不需要自己来创建“自动释放池块”。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们是自动释放池来包裹应用程序的主入口点 (main application entry point)。比如说,iOS 程序的 main 函数经常这样写:
1 int main (int argc, char *argv[]) { 2 3 @autoreleasepool { 4 5 return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate"); 6 } 7 }
从技术角度看,不是非得有个“自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由 UIApplicationMain 函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:
1 @autoreleasepool { 2 3 NSString *string = [NSString stringWithFormat:@"1= %i", 1]; 4 5 @autoreleasepool { 6 7 NSNumber *number = [NSNumber numberWithInt:1]; 8 } 9 10 }
本例中有两个对象,它们都是由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。NSString 对象放在外围的自动释放池中,而 NSNumber 对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
考虑下面这段代码:
1 for (int i = 0; i< 100000; i++) { 2 3 [self doSomethingWithInt:i]; 4 5 }
如果 “doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能会放在自动释放池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行 for 循环时,会持续有新的对象创建出来,并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
这种情况不甚理想,尤其当循环长度无法预知,必须取决于用户输入时更是如此。比方说,要从数据库中读出许多对象。代码可能会这么写:
1 NSArray *databaseRecords = /*...*/; 2 3 NSMutableArray *people = [NSMutableArray new]; 4 5 for(NSDictionary *record in databaseRecords) { 6 7 EOCPerson *person = [ [EOCPerson alloc] initWithRecord:record]; 8 [people addObject:person]; 9 }
EOCPerson 的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如:
1 NSArray *databaseRecords = /*...*/; 2 3 NSMutableArray *people = [NSMutableArray new]; 4 5 for (NSDictionary *record in databaseRecores) { 6 7 @autoreleasepool { 8 9 EOCPerson *perosn = [[EOCPerson alloc] initWithRecord:record]; 10 [people addObject:person]; 11 12 } 13 14 }
加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像 “栈”(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
如果在 ARC 出现之前就写过 Objective-C 程序,那么可能还记得有种老式写法,就是使用 NSAutoreleasePool 对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池,就像新语法中的自动释放池块一样。但是这种写法并不会在每次执行 for 循环时都清空池,此对象更为 “重量级”(heavyweight),通常用来创建那种偶尔要清空的池,比方说:
1 NSArray *databaseRecords = /*...*/; 2 3 NSMutableArray *people = [NSMutableArray new]; 4 5 int i = 0; 6 7 8 9 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 10 11 for(NSDictionary *record in databaseRecords) { 12 13 EOCPerson *person = [[EOCPerson alloc] initWithRecord:record]; 14 [people addobject:person]; 15 16 // Drain the pool only every 10 cycles 17 18 if (++i == 10) { 19 20 [pool drain]; 21 22 i = 0; 23 24 } 25 26 } 27 28 // Also drain at the end in case the loop is not a multiple or 10 29 30 [pool drain];
现在不需要这样写代码了。采用随着 ARC 所引入的新语法,可以创建出更为 “轻量级” (lightweight)的自动释放池。原来所写的代码可能会每执行 n 次循环清空一次自动释放池,现在可以改用自动释放池块把 for 循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。
@autoreleasepool 语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码:
1 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 2 3 id object = [self createObject]; 4 5 [pool drain]; 6 7 [self useObject:object];
这样写虽然稍显夸张,但却能说明问题。调用 “useObject:” 方法时所传入的那个对象,可能已经为系统所回收了。同样的代码改为新式写法就变成了:
1 @autoreleasepool { 2 3 id object = [self createObject]; 4 5 } 6 7 [self useObject:object];
这次根本就无法编译,因为 object 变量出了自动释放池块的外围后就不可用了,所以在调用 “useObject:”方法时不能用它做参数。
END