美文网首页
第三十四条: 以"自动释放池块"降低内存峰值

第三十四条: 以"自动释放池块"降低内存峰值

作者: iOS博仔 | 来源:发表于2021-12-03 14:35 被阅读0次

    Objective-C对象的生命期取决于其引用计数(参见第29条)。在objective-C的引用计数架构中,有一项特性叫做"自动释放池"(autorelease pool)。释放对象有两种方式: 一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入"自动释放池"中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送release消息。
    创建自动释放池所用语法如下:

@autoreleasepool{
      // ...
}

    如果在没有创建自动释放池的情况下给对象发送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函数里,我们用自动释放池来包裹应用程序的主入口点(mian application entry point )。比方说,iOS程序的main函数经常这样写:

int main (int argc, char *argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate");
    }
 }

    从技术的角度看,不是非得有个“自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占有的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由UIApplicationMain函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
    下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:

@autoreleasepool  {
    NSString *string = [NSString stringWithFormat:@"l = %i", l];
    @autoreleasepool {
            NSNumber *number = [NSNumber numberWithInt:l];
    }
}
本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。NSString对象放在外围的自动释放池中,而NSNumber对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
考虑下面这段代码:
 for (int i = 0; i < 100000 ; i ++) {
        [self doSomethingWithInt:i];
  }

如果 “doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能会放在自动池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行for循环时,会持续有新对象创建出来,并加入自动释放池中。所有这种对象都要等for循环执行完才会释放。这样一来,在执行for循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
这种情况不甚理想,尤其当循环长度无法预知,必须取决于用户输入时更是如此。比方说,要从数据库中读出许多对象。代码可能会这么写:

    NSArray *databaseRecords = /* ... */;
    NSMutableArray *people = [NSMutableArray new];
    for (NSDictionary *record in databaseRecords) {
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
EOCPerson的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如:
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [[NSMutableArray alloc] init];
for (NSDictionary *record in databaseRecords) {
      @autoreleasepool {
          EOCPerson *person =  [[EOCPerson alloc] initWithRecord:record];
          [people addObject:person];
      }
 }
加上这个自动释放池之后,应用程序在执行循环时内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像“栈(stack)”一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
如果在ARC出现之前就写过Objective-C程序,那么可能还记得有种老式写法,就是使用NSAutoreleasePool对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池就像新语法中的自动释放池一样。但是这种写法并不会在每次执行for循环时都清空池,此对象更为“重量级”(heavyweight),通常用来创建那种偶尔需要清空的池,比方说:
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [[NSMutableArray alloc] init];
int i = 0;

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (NSDictionary *record in databaseRecords) {
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];

    //Drain the pool only every 10 cycles
    if(++i == 10){
        [pool drain];
         i = 0;
    }
}
//   Also drain at the end in case the loop is not a multiple of 10 
[pool drain];
现在不需要再这样写代码了。采用随着ARC所引入的新语法,可以创建出更为“轻量级”(lightweight)的自动释放池。原来所写的代码可能会每执行n次循环清空一次自动释放池,现在可以改用自动释放池块把for循环中的语句也包起来,这样的话,每次执行循环时都会自动建立并清空自动释放池。
@autoreleasepool语法有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id object = [self createObject];
[pool drain];
[self useObject:object];
这样写虽然稍显夸张,但却能说明问题。调用“useObject:”方法时所传入的那个对象,可能已经为系统所回收了。同样的代码改用新式写法就变成了:
@autoreleasepool {
    id object = [self createObject];
}
[self useObject:object];
这次根本就无法编译,因为object变量出了自动释放池块的外围后就不可用了,所以在调用“useObject:”方法时不能用它做参数。
要点
  • 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。

相关文章

网友评论

      本文标题:第三十四条: 以"自动释放池块"降低内存峰值

      本文链接:https://www.haomeiwen.com/subject/mxapxrtx.html