第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

 

posted @ 2017-08-12 05:25  鳄鱼不怕牙医不怕  阅读(408)  评论(0编辑  收藏  举报