实用内存管理
本文为您提供了一种透视内存管理的实用性视角。这部分内容涵盖了“对象所有权和销毁”中介绍的基本概念,不过采用了更加面向代码实现的视角。
遵从以下几条简单的规则可以使内存管理变得更加容易,而不遵守这些规则将几乎肯定会在某些时候导致内存泄漏,或者由于消息被发送给已释放的对象而导致运行时异常。
为了让应用程序的内存消耗尽可能低,您应该清除掉不使用的对象,但是您需要确保您清除的不是正在被使用的对象。因此,您需要一种机制,可以让您标记那些仍然有用的对象。所以,从许多方面来讲,站在“对象的所有权”的角度看内存管理是最好理解的。
§ 一个对象可以有一个或一个以上的所有者。
采用类比的方式,您可以联想一个分时租用公寓。
§ 当一个对象没有所有者的时候,它会被销毁。
继续用类比的方法,您可以联想一个分时合租的寓所,但当地居民并不喜欢它。如果没有所有者,这处合租寓所将被拆除。
§ 为了确保您感兴趣的对象不被销毁,您必须成为它的一个所有者。
您可以建造一所新公寓,或入住一所现有的公寓。
§ 为了让您不再感兴趣的对象能够被销毁,您应该释放它的所有权。
您可以出售您的分时租用公寓。
为了支持这个模型,Cocoa提供了一种被称为“引用计数”或“保留计数”的机制。每一个对象都有一个保留计数。当对象被创建的时候,其保留计数为1
。当保留计数减少至0
时,对象会被回收(销毁)。您可以使用各种方法来操作保留计数(即获取或释放所有权):
alloc
为对象分配内存并返回该对象,其保留计数为1
。
您拥有以单词alloc
或new
开头的任意方法创建的对象。
copy
为对象创建一份副本并返回该对象,其保留计数为1
。
如果您复制一个对象,您就拥有了这个对象的副本。这对于任何名字中包含单词copy
的方法都是适用的,这里的“copy”是指被返回的对象。
retain
使一个对象的保留计数增加1
。
获得一个对象的所有权。
release
使一个对象的保留计数减少1
。
释放一个对象的所有权。
autorelease
使一个对象的引用计数在未来的某个阶段减少1
。
在未来的某个阶段释放一个对象的所有权。
内存管理的实用规则如下(也可以参考“内存管理规则”):
§ 您只拥有那些您使用名字以“alloc”或“new”开头或者名字中包含“copy”的方法(例如alloc,newObject
或mutableCopy)创建的对象,或者是那些收到了您发送的retain消息的对象。
许多类提供了形如+className...
的方法,您可以使用它们获得该类的一个新的实例。这些方法通常被称为“简便构造函数”,它们创建一个新的类的实例,对其进行初始化并将其返回供您使用。您并不拥有从简便构造函数或其它存取方法返回的对象。
§ 一旦您使用完一个您拥有的对象,您应该使用release
或autorelease
释放这个对象的所有权。
通常,您应该使用release
,而不是autorelease
。只有在不适合立即回收对象的情况下,您才应该使用autorelease
,比如您要从某个方法返回对象。(注意:这并不是说release
必然会引起对象的回收—只有当保留计数减少至0
时才会发生回收—但它也有可能会发生,而有时您需要防止出现这种情况,请参考“从方法返回对象”中的例子。)
§ 实现dealloc
方法来释放您拥有的实例变量。
§ 您不应该直接调用dealloc
(除非是您在自定义的dealloc
方法中调用超类的实现)。
下面几个简单的例子对比说明了使用alloc
,简便构造函数和存取方法创建一个新对象。
第一个例子使用alloc
创建了一个新的字符串对象。因此,它必须被释放。
- (void)printHello { |
NSString *string; |
string = [[NSString alloc] initWithString:@"Hello"]; |
NSLog(string); |
[string release]; |
} |
第二个例子使用简便构造函数创建了一个新的字符串对象。此外没有额外的工作要做。
- (void)printHello { |
NSString *string; |
string = [NSString stringWithFormat:@"Hello"]; |
NSLog(string); |
} |
第三个例子使用存取方法获取一个字符串对象。与简便构造函数一样,没有额外的工作要做。
- (void)printWindowTitle { |
NSString *string; |
string = [myWindow title]; |
NSLog(string); |
} |
虽然使用存取方法有时看似繁琐,有故意卖弄之嫌,但如果您坚持使用存取方法,则内存管理方面出现问题的可能性将大大减小。如果您在代码中对实例变量全部使用retain
和release
,那么几乎可以肯定您在做错误的事情。
考虑一个“计数器”对象(Counter),您要设置它的计数。
@interface Counter : NSObject { |
NSNumber *count; |
} |
为了获取和设置计数的值,您需要定义了两个存取方法。(下面的例子给出了存取方法的简单实现。在“存取方法”中有对它们更加详细的介绍。)在get方法中,您只是回传了一个变量,所以没有必要进行retain
或release
:
- (NSNumber *)count { |
return count; |
} |
在set方法中,如果其他人都按照同样的规则进行操作,则您需要假设新的计数可能会在任何时刻被销毁,因此您需要获得对象的所有权—向它发送一条retain
消息—来确保它不会被销毁。在这里您还需要通过向旧的计数对象发送一条release
消息来释放它的所有权。(Objective-C中允许向nil
发送消息,因此在计数尚未被设置时仍然可以这么做。)您必须在[newCount retain]
之后发送这个消息,以防这两者是同一个对象—您肯定不希望由于疏忽造成对象意外被回收。
- (void)setCount:(NSNumber *)newCount { |
[newCount retain]; |
[count release]; |
// make the new assignment |
count = newCount; |
} |
只有在两处地方您不该使用存取方法来设置实例变量—init
方法和dealloc
。为了用一个表示零的数字对象初始化一个计数对象,您可以按照下面的方式实现一个init
方法:
- init { |
self = [super init]; |
if (self) { |
count = [[NSNumber alloc] initWithInteger:0]; |
} |
return self; |
} |
为了用一个非零的计数初始化计数器,您可以这样实现一个initWithCount:
方法:
- initWithCount:(NSNumber *)startingCount { |
self = [super init]; |
if (self) { |
count = [startingCount copy]; |
} |
return self; |
} |
由于“计数器”类(Counter)有一个对象实例变量,您还必须实现一个dealloc
方法。该方法应该可以通过向实例变量发送release
消息来释放任何实例变量的所有权,并且最终调用超类的dealloc
实现:
- (void)dealloc { |
[count release]; |
[super dealloc]; |
} |
实现重置方法
假设您想实现一个方法来重置计数器。那么您有两种选择,第一种是使用简便构造函数创建一个新的NSNumber
对象—也因此没有必要发送任何retain
或release
消息。请注意,两种方法都使用了类的set存取方法。
- (void)reset { |
NSNumber *zero = [NSNumber numberWithInteger:0]; |
[self setCount:zero]; |
} |
第二种是使用alloc
创建NSNumber
实例,因此您要相应地使用release
。
- (void)reset { |
NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; |
[self setCount:zero]; |
[zero release]; |
} |
常见错误
下面几小节举例说明常见的错误。
没有使用存取方法
下面的例子在一些简单的情况下几乎肯定可以正常工作,但这个例子避免使用存取方法,这样做几乎肯定会在某个阶段(当您忘记保留或释放,或者当您的实例变量的内存管理语义发生变化的时候)导致错误。
- (void)reset { |
NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; |
[count release]; |
count = zero; |
} |
另外请注意,如果您正在使用键值观察(参考键值观察编程指南),那么用这种方式改变变量是不兼容KVO的。
实例泄露
- (void)reset { |
NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; |
[self setCount:zero]; |
} |
新数字的保留计数是1(来自alloc
),而且在该方法释放的作用域内没有与之对应的release。新数字是不可能被释放的,这将导致内存泄漏。
向非您所有的实例发送 release
- (void)reset { |
NSNumber *zero = [NSNumber numberWithInteger:0]; |
[self setCount:zero]; |
[zero release]; |
} |
如果没有调用retain
,则在当前的自动释放池被释放后,下一次您访问count
会失败。简便构造方法返回一个会自动释放的对象,所以您不必再发送release。这样做意味着,当因autorelease
而产生的release
被发送后,保留计数会被减为0,且对象将被释放。当您下次想要访问计数时,您将向一个已经被释放的对象发送消息(这时通常您会得到一个SIGBUS 10
错误)。
使用集合
当您把一个对象添加到一个集合,比如数组,字典或集合,集合拥有对象的所有权。当对象从集合中删除或集合本身被释放时,集合会释放所有权。因此,举例来说,如果您想创建一个数字数组,您可以选择以下方法中的一种:
NSMutableArray *array; |
NSUInteger i; |
// ... |
for (i = 0; i < 10; i++) { |
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i]; |
[array addObject:convenienceNumber]; |
} |
在这段代码中,您没有调用alloc
,因此也没有必要调用release
。没有必要保留新的数字对象(convenienceNumber
),因为数组会为您代劳。
NSMutableArray *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
消息。如果它们被删除,您还必须相应地发送release
消息,并且在您自己的dealloc
方法中,您还应该向其余的对象发送release
消息。
从方法返回的对象
当您从一个方法中返回一个局部变量时,您不仅要保证自己遵守了内存管理规则,而且要保证接收方在对象被释放之前一直有机会使用该对象。当您返回一个新创建的(您拥有的)对象时,您应该是用autorelease
而不是release
来释放所有权。
请考虑一个很简单的fullName
方法,用它来连接firstName
和lastName
。一种可行的正确的实现方法(仅仅从内存管理的角度而言—当然从功能性的角度考虑,它仍有很多不足之处)可能如下面的代码所示:
- (NSString *)fullName { |
NSString *string = [NSString stringWithFormat:@"%@ %@", firstName, lastName]; |
return string; |
} |
按照最基本的规则,您并不拥有stringWithFormat
返回的字符串,所以它可以安全地从该方法中返回。
下面这种实现方法也是正确的:
- (NSString *)fullName { |
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@", firstName, lastName] autorelease]; |
return string; |
} |
您拥有alloc
返回的字符串,但您随后向它发送了一条autorelease
消息,因此在您失去它的引用之前,您已经放弃了所有权,并且这样做也是满足内存管理规则的。这种实现方法的精髓在于使用了autorelease
而不是release
,要意识到这一点。
相比之下,下面的代码是错误的:
- (NSString *)fullName { |
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@", firstName, lastName] release]; |
return string; |
} |
纯粹从内存管理的角度来讲,它看起来是正确的:您拥有alloc
返回的字符串,并向它发送一条release
的信息来释放所有权。然而从实用角度来看,该字符串很有可能在那一步就被回收了(它可能没有任何其他的所有者),因此该方法的调用者会接收到一个无效的对象。这说明了为什么autorelease
非常实用—它能让您推迟释放,您可以在未来的某一时刻过后再释放对象。
为了追求完整性,下面的代码也是错误的:
- (NSString *)fullName { |
NSString *string = [[NSString alloc] initWithFormat:@"%@ %@", firstName, lastName]; |
return string; |
} |
您拥有alloc
返回的字符串,但是在您有机会释放所有权之前,您就失去了对该对象的引用。根据内存管理规则,这将导致内存泄漏,因为调用者没有得到任何迹象表明他们拥有返回的对象。