实用内存管理
本文为您提供了一种透视内存管理的实用性视角。这部分内容涵盖了“对象所有权和销毁”中介绍的基本概念,不过采用了更加面向代码实现的视角。
遵从以下几条简单的规则可以使内存管理变得更加容易,而不遵守这些规则将几乎肯定会在某些时候导致内存泄漏,或者由于消息被发送给已释放的对象而导致运行时异常。
为了让应用程序的内存消耗尽可能低,您应该清除掉不使用的对象,但是您需要确保您清除的不是正在被使用的对象。因此,您需要一种机制,可以让您标记那些仍然有用的对象。所以,从许多方面来讲,站在“对象的所有权”的角度看内存管理是最好理解的。
§ 一个对象可以有一个或一个以上的所有者。
采用类比的方式,您可以联想一个分时租用公寓。
§ 当一个对象没有所有者的时候,它会被销毁。
继续用类比的方法,您可以联想一个分时合租的寓所,但当地居民并不喜欢它。如果没有所有者,这处合租寓所将被拆除。
§ 为了确保您感兴趣的对象不被销毁,您必须成为它的一个所有者。
您可以建造一所新公寓,或入住一所现有的公寓。
§ 为了让您不再感兴趣的对象能够被销毁,您应该释放它的所有权。
您可以出售您的分时租用公寓。
为了支持这个模型,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
返回的字符串,但是在您有机会释放所有权之前,您就失去了对该对象的引用。根据内存管理规则,这将导致内存泄漏,因为调用者没有得到任何迹象表明他们拥有返回的对象。
分享到:
相关推荐
这儿的运行时系统扮演的角色类似于Objective-C语言的操作系统,Objective-C基于该系统来工作。本文档将具体介绍NSObject类以及Objective-C程序是如何和运行时系统交互的。特别地,本文档还给出来怎样在运行时动态地加
objective-c运行时编程指南,
Objective-C高级编程 iOS与OS X多线程和内存管理.pdf
Objective-C高级编程 iOS与OS X多线程和内存管理.
《Objective-C 程序设计(第4版)》已经为iOS 5和Xcode4.2中的重大变更做了全面更新,最大的改动是引入了自动引用计数(ARC),并详细说明了如何在Objective-C编程过程中使用ARC提升和简化内存管理。
帮助学习关于OC中内存管理的知识点
objective-c 内存管理 alloc init release
Objective-C内存管理 刚接触的的人可能有些迷惑,看了本文 你将成为Objective-C内存管理高手 文字 高清版
Objective-C 2.0运行时系统编程指南,有用指数5颗星。
Objective-C 内存管理 深入浅出发,熟悉内存管理。
第7章到第10章讲述objective-c的基础框架,以及文件操作、内存管理、数据保存等内容。第11章讲述了应用工具框架。第12、13章分别讲述了如何开发iphone/ipad应用程序。第14章讲述了objective-c++和访问mysql数据库的...
Objective-C语言的许多决策可以在编译和运行时执行。只要有可能,它是动态的。这意味着Objective-C语言不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。Runtime系统是一种用于Objective-C语言的操作系统...
初学objectice-C的朋友都有一个困惑,总觉得对objective-C的内存管理机制琢磨不透,程序经常内存泄漏 或莫名其妙的崩溃。我在这里总结了自己对objective-C内存管理机制的研究成果和经验,写了这么一个由 浅入深的...
高清最新完整版 Objective-C编程全解 第3版
Objective-C是一种面向对象的编程语言,广泛应用于Mac OS和iOS开发。通过学习Objective-C的基本语法、类和对象、控制流程和方法等内容,你将能够编写简单的Objective-C程序并逐渐掌握更复杂的概念。继续学习和实践,...
详细介绍 objective-c内存管理和原理剖析
objective-c 面向对象编程 ,主要讲objiectve-c 的面向对象方面。