(转) 从 C++ 到 Objective-C(14):内存管理(续)

autorelease 池

上一节中我们了解到 autorelease 的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解 autorelease 的机制。所以我们要从这里开始。当对象收到 autorelease 消息的时候,它会被注册到一个“autorelease 池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?

答案是丰富多彩的:如果你使用 Cocoa 开发 GUI 界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。

拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用 Cocoa 创建 GUI 程序时,这个 autorelease 池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用 retain 消息。

另一方面,如果没有 GUI,你必须自己建立 autorelease 池。当对象收到 autorelease 消息时,它能够找到最近的 autorelease 池。当池可以被清空时,你可以对这个池使用 release 消息。一般的,命令行界面的 Cocoa 程序都会有如下的代码:

int main(int argc, char* argv[]) {     NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    //...     [pool release];
    return 0;}

注意在 Mac OS X 10.5 的 NSAutoreleasePool 类新增加了一个 drain 方法。这个方法等价于:当垃圾收集器可用时做 release 操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。Mac OS 的新版本都会支持垃圾收集器,但是 iOS 却不支持。在引用计数环境下,NSAutoreleasePool 的 release 方法会给池中的所有对象发送 release 消息,如果对象注册了多次,就会多次给它发 release。drain 和 release 在应用计数环境下是等价的。在垃圾收集的环境下,release 不做任何事情,drain 则会触发垃圾收集。

使用多个 autorelease 池

在一个程序中使用多个 autorelease 池也是可以的。对象收到 autorelease 消息时会注册到最近的池。因此,如果一个函数需要创建并使用很大数量临时对象,为了提高性能,可以创建一个局部的 autorelease 池。这种情况下,这些临时变量就可以及时的被销毁,从而在函数返回时就将内存释放出来。

autorelease 的注意点

使用 autorelease 可能会有一些误用情况,需要我们特别注意。

 

  • 首先,非必要地发送多个 autorelease 类似发送多个 release 消息,在内存池清空时会引起内存错误;
  • 其次,即使 release 可以由 autorelease 替代,也不能滥用 autorelease。因为 autorelease 要比正常的 release 消耗资源更多。另外,不必要的推迟 release 操作无疑会导致占用大量内存,容易引起内存泄露。

autorelease 和 retain

多亏了 autorelease,方法才能够创建能够自动释放的对象。但是,长时间持有对象是一种很常见的需求。在这种情形下,我们可以向对象发送 retain 消息,然后在后面手动的 release。这样,这个对象实际上可以从两个角度去看待:

  • 从函数开发者的角度,对象的创建和释放都是有计划的;
  • 从函数调用者的角度,使用了 retain 之后,对象的生命期变长了(使用 retain 将使其引用计数器加 1),为了让对象能够正确地被释放,调用者必须负责将计数器再减 1。

我们来理解一下这句话。对于一个函数的开发者,如果他不使用 autorelease,那么,他使用 alloc 创建了一个对象并返回出去,那么,他需要负责在合适的时候对这个对象做 release 操作。也就是说,从函数开发者的角度,这个对象的计数器始终是 1,一次 release 是能够被正常释放的。此时,函数调用者却使用 retain 将计数器加 1,但是开发者不知道对象的计数器已经变成 2 了,一次 release 不能释放对象。所以,调用者必须注意维护计数器,要调用一次 release 将其恢复至 1。

Convenience constructor, virtual constructor

将构造对象的过程分成 alloc 和 init 两个阶段,有时候显得很罗嗦。好在我们有一个 convenience constructor 的概念。这种构造函数应该使用类名做前缀,其行为类似 init,同时要实现 alloc。但是,它的返回对象需要注册到一个内部的 autorelease 池,如果没有给它发送 retain 消息时,这个对象始终是一个临时对象。例如:

// 啰嗦的写法 NSNumber* zero_a = [[NSNumber alloc] initWithFloat:0.0f];
...[zero_a release];
...// 简洁一些的 NSNumber* zero_b = [NSNumber numberWithFloat:0.0f];
...// 不需要 release

根据我们前面对内存管理的介绍,这种构造函数的实现是基于 autorelease 的。但是其底层代码并不那么简单,因为这涉及到对 self 的正确使用。事实上,这种构造函数都是类方法,所以 self 指向的是 Class 类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self 指向的是这个类的对象的实例,也就是一个“普通的”对象。

编写错误的这种构造函数是很容易的。例如,我们要创建一个 Vehicle 类,包含一个 color 数据,编写如下的代码:

// The Vehicle class @interface  Vehicle : NSObject {     NSColor* color;}  -(void) setColor:(NSColor*)color;
 // 简洁构造函数 +(id) vehicleWithColor:(NSColor*)color;
 @end 

其对应的实现是:

// 错误的实现 +(Vehicle*) vehicleWithColor:(NSColor*)color{     // self 不能改变     self = [[self alloc] init]; // 错误!     [self setColor:color];
    return [self autorelease];}

记住我们前面所说的,这里的 self 指向的是 Class 类型的对象。

// 比较正确的实现 +(id) vehicleWithColor:(NSColor*)color{     id newInstance = [[Vehicle alloc] init]; // 正确,但是忽略了有子类的情况     [newInstance setColor:color];
    return [newInstance autorelease];}

我们来改进一下。Objective-C 中,我们可以实现 virtual constructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个 class 方法(注意,class 在 Objective-C 中不是关键字);这是 NSObject 的一个方法,返回当前对象的类对象(也就是 meta-class 对象)。

@implementation Vehicle
 +(id) vehicleWithColor:(NSColor*)color{     id newInstance = [[[self class] alloc] init]; // 完美!我们可以在运行时识别出类     [newInstance setColor:color];
    return [newInstance autorelease];}  @end   @interface  Car : Vehicle {...} @end  ...// 创建一个 red Car id car = [Car vehicleWithColor:[NSColor redColor]];

类似于初始化函数的 init 前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如 [NSColor redColor] 返回一个预定义的颜色,按照我们的约定,使用 [NSColor colorRed] 更合适一些。

最后,我们要重复一下,所有使用 alloc、[mutable]copy[WithZone:] 增加引用计数器值的对象,都必须相应地调用 [auto]release。当调用简洁构造函数时,你并没有显式调用 alloc,也就不应该调用 release。但是,在创建这种构造函数时,一定不要忘记使用 autorelease。

转载于:https://my.oschina.net/yongbin45/blog/64984

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值