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

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

5 2K

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 却不支持。在引用计数环境下,NSAutoreleasePoolrelease方法会给池中的所有对象发送release消息,如果对象注册了多次,就会多次给它发releasedrainrelease在应用计数环境下是等价的。在垃圾收集的环境下,release不做任何事情,drain则会触发垃圾收集。

使用多个 autorelease 池

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

autorelease的注意点

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

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

autoreleaseretain

多亏了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

5 评论

willonboy's blog 2011年4月4日 - 16:57

http://www.cocoachina.com/bbs/read.php?tid-52786.html

这页面中的问题不知道你是怎么理解的

回复
DevBean 2011年4月5日 - 12:34

我没有注意这个问题,但是有一个建议是不要纠结于这个数值。因为在 Apple 的文档中有这么一段说明:

Important: This method is typically of no value in debugging memory management issues. Because any number of framework objects may have retained an object in order to hold references to it, while at the same time autorelease pools may be holding any number of deferred releases on an object, it is very unlikely that you can get useful information from this method.

framework 中的实现细节对高层应用程序员而言是不透明的,你不知道它里面是怎么运作的,毕竟它里面可能会有很多的 retain。你所能做的就是将retain/copy 和 release/autorelease 对应起来,因为 framework 对我们保证其内部不会造成泄露(如果没有 bug 的话),关键点是我们自己的代码。所以这个 retainCount 对于内存管理的意义不大。

回复
willonboy's blog 2011年4月4日 - 17:10

// 比较正确的实现
+(id) vehicleWithColor:(NSColor*)color
{....}

在NSString中有[NSString stringWithString:@"string"]方法 我在打印它产生实例对象的内存地址时发现它似乎不是在堆栈中分配的 这个是否可以说明 "临时对象"在内存分配时是不同于正常情况的呢?

回复
willonboy's blog 2011年4月4日 - 17:12

进而是否可以说明autorelease的变量内存地址分配方式 不是在堆栈中分配区块的!?

回复
Define_feeling 2011年6月22日 - 15:29

这个说的很对

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2