`
linuxstuding
  • 浏览: 1229661 次
文章分类
社区版块
存档分类
最新评论

Objective-C内存管理编程指南(3)实用内存管理

 
阅读更多

实用内存管理

本文为您提供了一种透视内存管理的实用性视角。这部分内容涵盖了对象所有权和销毁中介绍的基本概念,不过采用了更加面向代码实现的视角。

遵从以下几条简单的规则可以使内存管理变得更加容易,而不遵守这些规则将几乎肯定会在某些时候导致内存泄漏,或者由于消息被发送给已释放的对象而导致运行时异常。

基础知识

为了让应用程序的内存消耗尽可能低,您应该清除掉不使用的对象,但是您需要确保您清除的不是正在被使用的对象。因此,您需要一种机制,可以让您标记那些仍然有用的对象。所以,从许多方面来讲,站在对象的所有权的角度看内存管理是最好理解的。

§ 一个对象可以有一个或一个以上的所有者。

采用类比的方式,您可以联想一个分时租用公寓。

§ 当一个对象没有所有者的时候,它会被销毁。

继续用类比的方法,您可以联想一个分时合租的寓所,但当地居民并不喜欢它。如果没有所有者,这处合租寓所将被拆除。

§ 为了确保您感兴趣的对象不被销毁,您必须成为它的一个所有者。

您可以建造一所新公寓,或入住一所现有的公寓。

§ 为了让您不再感兴趣的对象能够被销毁,您应该释放它的所有权。

您可以出售您的分时租用公寓。

为了支持这个模型,Cocoa提供了一种被称为引用计数保留计数的机制。每一个对象都有一个保留计数。当对象被创建的时候,其保留计数为1。当保留计数减少至0时,对象会被回收(销毁)。您可以使用各种方法来操作保留计数(即获取或释放所有权):

alloc

为对象分配内存并返回该对象,其保留计数为1

您拥有以单词allocnew开头的任意方法创建的对象。

copy

为对象创建一份副本并返回该对象,其保留计数为1

如果您复制一个对象,您就拥有了这个对象的副本。这对于任何名字中包含单词copy的方法都是适用的,这里的“copy”是指被返回的对象。

retain

使一个对象的保留计数增加1

获得一个对象的所有权。

release

使一个对象的保留计数减少1

释放一个对象的所有权。

autorelease

使一个对象的引用计数在未来的某个阶段减少1

在未来的某个阶段释放一个对象的所有权。

内存管理的实用规则如下(也可以参考内存管理规则):

§ 您只拥有那些您使用名字以“alloc”“new”开头或者名字中包含“copy”的方法(例如allocnewObjectmutableCopy)创建的对象,或者是那些收到了您发送的retain消息的对象。

许多类提供了形如+className...的方法,您可以使用它们获得该类的一个新的实例。这些方法通常被称为简便构造函数,它们创建一个新的类的实例,对其进行初始化并将其返回供您使用。您并拥有从简便构造函数或其它存取方法返回的对象。

§ 一旦您使用完一个您拥有的对象,您应该使用releaseautorelease释放这个对象的所有权。

通常,您应该使用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);
}

使用存取方法

虽然使用存取方法有时看似繁琐,有故意卖弄之嫌,但如果您坚持使用存取方法,则内存管理方面出现问题的可能性将大大减小。如果您在代码中对实例变量全部使用retainrelease,那么几乎可以肯定您在做错误的事情。

考虑一个计数器对象(Counter),您要设置它的计数。

@interface Counter : NSObject {
 NSNumber *count;
}

为了获取和设置计数的值,您需要定义了两个存取方法。(下面的例子给出了存取方法的简单实现。在存取方法中有对它们更加详细的介绍。)在get方法中,您只是回传了一个变量,所以没有必要进行retainrelease

- (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对象也因此没有必要发送任何retainrelease消息。请注意,两种方法都使用了类的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方法,用它来连接firstNamelastName。一种可行的正确的实现方法(仅仅从内存管理的角度而言当然从功能性的角度考虑,它仍有很多不足之处)可能如下面的代码所示:

- (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返回的字符串,但是在您有机会释放所有权之前,您就失去了对该对象的引用。根据内存管理规则,这将导致内存泄漏,因为调用者没有得到任何迹象表明他们拥有返回的对象。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics