【译】苹果官方手册:高级内存管理编程手册3:实际内存管理
尽管在内存管理方法中提到的概念并不复杂,但还是存在一些实用的步骤可以使内存管理更加方便,并确保你的程序在实用最少的资源需求的时候的可靠性和稳定性。
实用访问方法使内存管理更方便
当你的类拥有一个对象作为属性时,你必须保证所有的对象在你还需要使用它们之前,不能被销毁。所以你必须在设置对象的时候就声明所有权。同时你必须确保放弃对即时引用的值的所有权。
有时候这看起来枯燥无味又墨守成规,但如果你一贯地使用访问方法,内存管理出错的几率会大大降低。如果你一直是在你的代码中使用retain和release管理引用变量,那么很可能你已经误入歧途。
考虑一下如何定义一个可以记录一种你想记录的数据的计数类。
@interface Counter : NSObject @property (nonatomic, retain) NSNumber *count; @end;
其中的property(声明和定义某个类的访问方法的速记法,同时你可以自己实现这些方法来替代默认的访问方法)声明了两个访问方法。一般情况下,编译器会自动实现这些方法;然而,了解这些方法究竟是怎样被实现的是很有意义的。
在“get”方法中,你仅仅返回引用变量,所以不需要使用retain或release:
- (NSNumber *)count { return _count; }
在“set”方法中,我们假定新的计数变量可能会在任意时刻被回收,所以需要发送一条retain消息确保它不会被回收掉,假设所有人都遵循这一条规则。同时你必须通过给旧的计数变量发送一条release消息来解除对它的所有权。(给一个nil对象发送消息在Objective-C中是没问题的,所以即使_count并没有被初始化过,这样实现也不会有问题。)你必须在[newCount retain]之后再发送这条消息给旧对象,避免被设置的新对象和旧对象其实是同一个对象这种情况——你肯定不希望这个对象被不经意地释放了。
- (void)setCount:(NSNumber *)newCount { [newCount retain]; [_count release]; _count = newCount; }
在属性值中使用访问方法
假设你准备实现一个重置计数器的方法。这里有好几种选择。第一种实现是使用alloc创建一个新的NSNumber引用,并使用release抵消其增加的计数。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [self setCount:zero]; [zero release]; }
第二种方法是使用一个便利构造来创建一个新的NSNumber对象。此时就不再需要retain或者release消息了。
- (void)reset { NSNumber *zero = [NSNumber numberWithInteger:0]; [self setCount:zero]; }
我们发现这都会使用“set”方法。
下边的代码在通常情况下是工作正常的,但因为它倾向于避免使用访问方法,这很可能会在某些情况下导致错误(例如,当你忘记了保持或释放,或者,当引用变量内存管理的含义发生变化的时候)。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [_count release]; _count = zero; }
同时要记住,面向键值对编程(面向键值对编程是提供一种当一个对象发生改变时,其他对象能直接被通知到的一种机制,简称KVO)是不兼容这种修改变量的方式的。
不要再初始化方法和dealloc方法中使用访问方法
唯一的你不应该使用访问方法来设置引用变量的地方是在初始化(一个类可能会定义多个初始化方法,从而使得它的初始化可以接受多种不同方式的输入值,或者,提供默认初始值从而给客户端提供更简单的初始化方法)方法和dealloc方法中。为了将计数对象的值设置为0,你可能会使用如下代码来实现init方法:
- init { self = [super init]; if (self) { _count = [[NSNumber alloc] initWithInteger:0]; } return self; }
为了将计数对象的值初始化为0以外的值,你可能会使用如下代码来实现一个initWithCount:方法:
- initWithCount:(NSNumber *)startingCount { self = [super init]; if (self) { _count = [startingCount copy]; } return self; }
由于Counter类含有一个引用对象变量,所以你必须实现一个dealloc方法。它将给每一个引用变量发送一条release方法来解除对它们的所有权,并在最后调用父类的实现。
- (void)dealloc { [_count release]; [super dealloc]; }
使用弱引用来避免保持循环
保持一个对象会创建一个对该对象的强引用。一个对象在强引用它的对象被释放之前不会被销毁。当两个对象被循环引用——也就是说,它们彼此拥有对方的一个强引用(可能是直接的互相引用,也可能是由一串对象间接造成的引用),这时候,就会出现一种被称作保持循环的问题。
图1所示的对象关系图就显示了一种间接保持循环的情况。Document对象对文档中的每一页都有一个Page对象。每个Page对象又有一个属性用来保存它们属于哪个文档。如果两者对彼此都有一个强引用,那么两者就都没办法被销毁。Document对象的引用计数在Page对象释放之前不会置0,并且Page对象在Document对象被销毁之前不会释放。
解决保持循环问题的方式就是使用弱引用。弱引用是一种非占有的关系,原对象含有目标对象的引用,但并不保持(retain)它。
为了保持对象图不被破坏,我们仍然需要在某些地方使用强引用(如果全是弱引用的话,Page类的对象和Paragraph的对象就会因为没有任何所有者而被销毁)。Cocoa建立了一种约定,一个“父”对象应该含有“子”对象的强引用,“子”对象应该保持对“父”对象的弱引用。
所以,在图1中,Document对象含有对Page对象的强引用,从而保持(retain)这些对象,Page对象含有对Document对象的弱引用,从而不会保持(retain)Document对象。
在Cocoa中,弱引用的例子包括但不限于表格数据源、缩略图组件、通知中心(给一个或多个观察者对象发送事件消息)观察者和混杂目标与委托(委托是一种简单但强大的设计模式,用来代表某个对象作出行为,或和其他对象合作)。
当给那些弱引用对象发送消息时,你需要更加小心。如果给一个已经被销毁的对象发送了消息,你的程序将会崩溃。你必须明确地了解对象何时是有效的。在大多数情况下,弱引用对象应该知道其他对象是对它进行弱引用的,比如循环引用的情况,并且应该有责任在它将要销毁的时候通知其他对象。例如,当你给通知中心注册了一个对象,通知中心将保存一个该对象的弱引用,并在相关的通知传过来的时候给它发送消息。当这个对象销毁的时候,你需要在通知中心中注销它,从而避免通知中心还会向这个已经不存在的对象发送消息。同样地,当一个委托对象被销毁的时候,你需要通过发送一个包含nil参数的setDelegate:消息给被委托对象来移除委托关系。这些消息通常在dealloc方法中被发送。
避免你正使用中的的对象被销毁
Cocoa关于拥有权的法则指出,调用方法接收到的对象通常需要在整个作用域内保持有效。同样将接收到的对象从当前作用于返回的时候,也不应担心它会被释放。对你的程序而言,“getter"方法返回一个缓存的引用变量或计算后的值都没太大关系,关键是要在你需要使用它的时候,他应该保持有效。
接下来是这个条例中偶发的一场,主要由两类问题引起:
- 当一个对象从某种基本容器类(容器类是基础框架的一种对象,它的主要作用是使用线性表、词典或集合的方式保存一系列对象)中被移除时。
heisenObject = [array objectAtIndex:n]; [array removeObjectAtIndex:n]; // heisenObject 现在可能已经无效了
当一个对象从这种基本容器中被移除的时候,他将会收到一条release(或者是autorelease)消息。如果这个容器对象是被移除对象的唯一所有者时,那么被移除对象(本例是heisenObject)将会在之后立刻被销毁。 - 当一个“父”对象呗销毁时。
id parent = <#create a parent object#>; // ... heisenObject = [parent child] ; [parent release]; // 或者,例如:self.parent = nil; // heisenObject 现在可能已经无效了
在某些时候你会从其他对象那里取回一个对象,并在之后直接或间接地释放了那个对象的父对象。当父对象被释放从而被销毁的时候,如果他是这个子对象的唯一拥有者,那么子对象(本例中的heisenObject)也会同时被销毁(假设它在父对象的dealloc方法中收到的是release消息,而不是autorelease消息)。
为了防止这些情况,你应该在接收到heisenObject对象的时候保持它,并在完成工作后释放它。举例如下:
heisenObject = [[array objectAtIndex:n] retain]; [array removeObjectAtIndex:n]; // 使用 heisenObject... [heisenObject release];
不要使用dealloc管理稀有资源
通常你不应使用dealloc方法管理诸如文件描述符、网络连接和缓冲区高速缓存等这些稀有资源。尤其是你不应该在设计这些类时,在你认为应该调用dealloc的地方调用dealloc。由于会出现漏洞或销毁程序,对dealloc的调用应该被避开或延迟。
取而代之,如果你的程序里有这种管理了稀有资源的类,你应该在不再需要这些资源的时刻让这些类的对象进行一些“清理”。通常你可以释放引用,并随后dealloc,但如果它没有销毁,你也不会遇到其他问题。
当你尝试将资源管理附加在dealloc上时,经常会导致问题的发生。例如:
- 依赖对象图销毁的次序。
对象图的销毁是内部无序的。即使你可能通常会想要或设置一个确切的顺序,但这意味着你在增加程序的脆弱性。例如如果一个对象在不期望的时刻就被释放或自动释放了,那么销毁的顺序可能就会改变,这会导致不可预料的结果。 - 稀有资源不能被回收。
内存泄漏通常属于可修复的程序漏洞,但它们一般不是致命的。但如果稀有资源没有在你期望的时候释放,这将会导致更加严重的问题。例如,如果你的应用占用了所有的文件描述符,用户将无法保存数据。 - 在错误的线程上调用清理逻辑。
如果一个对象在非期望的时刻被自动释放了,那么所有线程的自动释放池都会释放它。这很容易使那些只允许一个线程接触的资源发生致命错误。
容器拥有他们包含的对象
当你向容器类(例如线性表、词典或集合)的对象中添加一个对象时,容器将获得对它的拥有权。在这个对象被移除或容器对象本身被释放的时候,这种依赖关系会被移除。例如,如果你想创建一个数组:
NSMutableArray *array = <#Get a mutable array#>; NSUInteger i; // ... for (i = 0; i < 10; i++) { NSNumber *convenienceNumber = [NSNumber numberWithInteger:i]; [array addObject:convenienceNumber]; }
在这种情况下,你不需要调用alloc,所以也不需要调用release。你并不需要保持新的数字(convenienceNumber),容器类自己就会做这些。
NSMutableArray *array = <#Get a mutable array#>; NSUInteger i; // ... for (i = 0; i < 10; i++) { NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i]; [array addObject:allocedNumber]; [allocedNumber release]; }
在这种情况下,在for循环的作用域内,你需要给allocedNumber发送一条release消息来平衡alloc消息造成的引用计数增加。由于在使用addObject:方法将它添加进容器的时候,容器对象已经保持了这个数字,所以这并不会导致容器中的数字被销毁。
为了理解这一点,你可以站在开发者的角度去考虑容器类的实现。你得确认不会有对象在添加进来之后突然消失了,所以在它们被添加进来的时候,你给他们发送了一条retain消息。在它们被移除出去的时候,为了平衡之前的retain,你应该再发送一条release消息。若要销毁整个容器,那么在整个容器对象被销毁之前,在dealloc方法里,所有容器内的对象都应该收到一条release消息。
所有权的法则使用保持计数实现
所有权的法则是使用引用计数——因为retain方法的缘故通常叫做“保持计数”,来实现的。每个对象都有一个保持计数。
- 当你创建一个新的对象时,保持计数置1.
- 当你给该对象发送retain消息时,保持计数+1.
- 当你给该对象发送release消息时,保持计数-1.
当你给该对象发送autorelease消息时,在当前自动释放池代码块结束时,保持计数-1. - 如果一个对象的保持计数置零,它将会被销毁。
重要提示:一般情况下没有需要明确查询一个对象的保持计数的原因(参考retainCount)。原因是你也许会忽略那些框架对象保持了一个你感性却的对象,这常常会造成误导。在调试内存管理的问题时,你应该只将精力集中在确保你的代码符合所有权的法则。