如何避免iPhone应用程序中的内存泄漏
一些避免 iPhone 应用内存泄漏的技巧。
引言
本文列出了一些避免 iPhone 应用内存泄漏的技巧。
所有权
所有权是 iPhone 上内存管理应如何运作的总体思路。当一个对象拥有所有者时,所有者负责在使用完该对象后释放它。一个对象可以有多个所有者,当它没有所有者时,它将被标记为待释放。
通过使用 alloc
、new
或 copy
创建对象、调用对象的 retain
方法,或者调用名称中包含 Create 或 Copy 的 Cocoa 函数时,就产生了所有权。内存的释放有两种方式:显式调用对象的 release
方法,或者使用自动释放池。
所有权背后是一个称为引用计数的系统。iPhone SDK 中的大多数对象都具有强引用,这意味着它们使用引用计数。
当你创建一个对象时,它的引用计数将为 1,调用对象的 retain
方法会将引用计数加 1。调用 release
方法会将引用计数减 1;当引用计数达到零时,对象将被释放。调用 autorelease
而不是 release
意味着对象将在稍后自动被释放。
对象也可以被弱引用,这意味着不会保留引用计数,并且对象需要手动释放。
何时应该使用 retain
?当你希望防止一个对象在你使用它之前被释放时。
每次使用 copy
、alloc
、retain
,或者名称中包含 Create 或 Copy 的 Cocoa 函数时,你都需要有一个匹配的 release
或 autorelease
。
开发者应该从所有权的视角思考对象,而不是担心引用计数。如果你有匹配的 retain
和 release
调用,那么引用计数的加 1 和减 1 操作自然是匹配的。
注意:使用 [object retainCount]
可能会产生误导,因为 SDK 中后台的代码可能会导致其返回值不准确。不建议用这种方式管理内存。
自动释放
设置为自动释放的对象意味着不需要显式释放它们,因为当自动释放池弹出时它们将被释放。iPhone 有一个在主线程上运行的自动释放池,它通常在事件循环结束时释放对象。当你创建自己的线程时,你必须创建自己的自动释放池。
在 iPhone 上,有便捷构造函数;使用便捷构造函数创建的对象会被设置为自动释放。
示例
NSString* str0 = @"hello";
NSString* str1 = [NSString stringWithString:@"world"];
NSString* str2 = str1;
可以通过以下方式将已分配的对象设置为自动释放
NSString* str = [[NSString alloc] initWithString:@"the flash?"];
[str autorelease];
或者像这样:
NSString* str = [[[NSString alloc] initWithString:@"batman!"] autorelease];
自动释放对象的归属权在指针超出范围时或自动释放池弹出时被放弃。
内置的自动释放池通常在事件循环结束时弹出,但这可能不适用于每个迭代都在分配大量内存的循环。在这种情况下,你可以在循环中创建一个自动释放池。自动释放池可以嵌套,因此在内部池中分配的对象将在该池弹出时被释放。在下面的示例中,对象将在每次迭代结束时被释放。
for (int i = 0; i < 10; ++i)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [NSString stringWithString:@"hello world"];
[self ProcessMessage: str];
[pool drain];
}
注意:在撰写本文时,iPhone 不支持垃圾回收,因此 drain
的作用与 release
相同。通常在希望将程序移植到 OSX,或者将来 iPhone 支持垃圾回收时使用 drain
。drain
会向垃圾回收器提示内存正在被释放。
返回对象指针
遵循所有权规则时,开发者需要了解哪些函数拥有某个对象的归属权。下面是一个返回对象指针并释放它的示例。
错误的方式
- (NSMutableString*) GetOutput
{
NSMutableString* output = [[NSMutableString alloc] initWithString:@"output"];
return output;
}
- (void) Test
{
NSMutableString* obj = [self GetOutput];
NSLog(@"count: %d", [obj retainCount]);
[obj release];
}
在此示例中,output
由 GetOutput
拥有。让 Test
释放 obj
违反了 Cocoa 内存管理指南中的规则;这不会导致内存泄漏,但不是好的做法,因为 Test
不应该释放它不拥有的对象。
如果调用 GetOutput
,调用者不应知道从 GetOutput
返回的对象是否被保留。因此,它可以自由地保留和释放返回的对象,而不会干扰应用程序中的任何其他代码。
正确的方式
- (NSMutableString*) GetOutput
{
NSMutableString* output = [[NSMutableString alloc] initWithString:@"output"];
return [output autorelease];
}
- (void) Test
{
NSMutableString* obj = [self GetOutput];
NSLog(@"count: %d", [obj retainCount]);
}
在第二个示例中,当 GetOutput
返回时,output
被设置为自动释放。 output
的引用计数被递减,GetObject
放弃了对 output
的所有权。Test
函数现在可以自由地保留和释放该对象,并确信在完成时不会发生泄漏。
在示例中,obj
被设置为自动释放,因此 Test
函数在函数结束时不会拥有它,但如果它想将对象存储在其他地方呢?
那么该对象就需要被新的所有者保留。
Setters
setter 函数必须保留它正在存储的对象,这意味着要声明所有权。如果我们想创建一个 setter 函数,在将新指针分配给我们的成员变量之前,我们需要做两件事。
在函数中
- (void) setName:(NSString*)newName
首先,我们将递减我们成员变量的引用计数
[name release];
这将允许 name
对象在引用计数为零时被释放,但它将允许该对象的任何其他所有者继续使用该对象。
然后,我们将递增新的 NSString
对象的引用计数
[newName retain];
因此,当 setName
选择器完成时,newName
不会被释放。newName
指向的对象与 name
指向的对象不同,引用计数也不同。
现在我们可以将 name
设置为指向 newName
对象
name = newName;
但是,如果 name
和 newName
是同一个对象怎么办?我们不能在可能导致其被释放后又保留它!
在释放已存储的对象之前,简单地保留传入的对象
[newName retain];
[name release];
name = newName;
现在,如果对象是相同的,它将增加引用计数,然后又从中减去,从而在赋值前保持不变。
另一种方法是使用 Objective-C 属性。
对象的属性声明如下
@property(nonatomic, retain) NSString *name;
nonatomic
表示没有阻塞,允许多个线程同时访问数据。atomic
会锁定数据以供单个线程访问,但速度较慢,因此除非必要,否则不使用。retain
表示我们希望保留newName
对象。
我们可以使用 copy
而不是 retain
@property(nonatomic, copy) NSString *name;
这等同于如下函数
- (void) setName:(NSString*)newName
{
NSString* copiedName = [newName copy];
[name release];
name = copiedName;
[name retain];
[copiedName release];
}
这里,newName
被复制到 copiedName
,现在 copiedName
拥有该字符串的副本。name
被释放,copiedName
被赋给 name
。然后 name
保留该字符串,因此 copiedName
和 name
都拥有它。最后,copiedName
释放该对象,name
成为该复制字符串的唯一所有者。
像这样的 setter 对于保留成员对象至关重要,如果我们有一个像这样的函数
- (void) Test
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// do something...
name = [self GetOutput];
// do something else...
NSLog(@"Client Name before drain: %@", name);
[pool drain];
NSLog(@"Client Name after drain: %@", name);
}
在调用 drain
后,name
将是未定义的,因为 name
将在池被排空时被释放。
如果我们用以下赋值替换
[self setName:[self GetOutput]];
那么 name
将由类拥有,并被保留以供使用,直到调用 release
。
但是我们何时释放对象?
由于 name
是一个成员变量,最安全的地方是在它所属类的 dealloc
函数中释放它。
- (void)dealloc
{
[name release];
[super dealloc];
}
注意:由于 dealloc
并不总是被调用,因此依赖在 dealloc
中释放对象来触发某些操作可能很危险。在程序退出时,iPhone OS 可能会在调用 dealloc
之前清除所有应用程序内存。
在用 setter 分配对象时,要小心类似这样的代码行
[self setName:[[NSString alloc] init]];
name
会被正确设置,但是 alloc
没有匹配的 release
。更好的方法是这样
NSString* s = [[NSString alloc] init];
[self setName:s];
[s release];
或者使用 autorelease
[self setName:[[[NSString alloc] init] autorelease]];
自动释放池
自动释放池将释放分配在它们分配和排空函数之间的对象。
在下面的函数中,我们有一个带循环的函数。在这个循环中,我们将 NSNumber
的副本赋给 magicNumber
。我们还设置 magicNumber
为自动释放。在此示例中,我们希望在每次迭代时排空池(这可以为具有大量赋值的循环节省内存)。
- (void) Test
{
NSString* clientName = nil;
NSNumber* magicNumber = nil;
for (int i = 0; i < 10; ++i)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
magicNumber = [[self GetMagicNumber] copy];
[magicNumber autorelease];
if (i == [magicNumber intValue])
{
clientName = [self GetOutput];
}
[pool drain];
}
if (clientName != nil)
{
NSLog(@"Client Name: %@", clientName);
}
}
问题是 clientName
在本地自动释放池中被分配和释放。因此,当循环结束时,clientName
已经被释放,并且对 clientName
的任何进一步使用都将是未定义的。
在这种情况下,我们可以在分配 clientName
后保留它,并在完成后释放它
- (void) Test
{
NSString* clientName = nil;
NSNumber* magicNumber = nil;
for (int i = 0; i < 10; ++i)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
magicNumber = [[self GetMagicNumber] copy];
[magicNumber autorelease];
if (i == [magicNumber intValue])
{
clientName = [self GetOutput];
[clientName retain];
}
[pool drain];
}
if (clientName != nil)
{
NSLog(@"Client Name: %@", clientName);
[clientName release];
}
}
我们已经从 retain
和 release
调用之间的时段获得了 clientName
的所有权。通过添加一对 retain
和 release
调用,我们表明 clientName
在显式调用 release
之前不会被释放。
集合
当一个对象被添加到集合中时,它将归集合所有。
在此示例中,我们分配了一个字符串;它现在有一个所有者
NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
然后我们将其添加到数组中;现在它有两个所有者
[array addObject: str];
我们可以安全地释放该字符串,使其仅由数组拥有
[str release];
当集合被释放时,它也会释放其所有对象。
NSMutableArray* array = [[NSMutableArray alloc] init];
NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
[array addObject: str];
[array release];
在上例中,我们分配了一个数组,分配了一个字符串,将字符串添加到数组,然后释放了数组。这使得字符串只有一个所有者,并且在调用 [str release]
之前它不会被释放。
在线程中传递指针
在此函数中,我们将字符串 input
传递给函数 DoSomething
,然后释放 input
。
- (void) Test
{
NSMutableString* input = [[NSMutableString alloc] initWithString:@"batman!"];
[NSThread detachNewThreadSelector:@selector(DoSomething:) toTarget:self withObject:input];
[input release];
}
detatchNewThreadSelector
会增加 input
对象的引用计数,并在线程完成时释放它。这就是为什么我们在启动线程后可以立即释放 input
,而不管函数 DoSomething
何时开始或结束。
- (void) DoSomething:(NSString*)str
{
[self performSelectorOnMainThread:@selector(FinishSomething:)
withObject:str waitUntilDone:false];
}
performSeclectorOnMainThread
也将保留传入的对象,直到选择器完成。
自动释放池是线程特定的,因此如果我们正在新线程上创建自动释放对象,我们需要创建一个自动释放池来释放它们。
[NSThread detachNewThreadSelector:@selector(Process) toTarget:self withObject:nil];
这会在另一条线程上调用函数 Process
。
- (void) Process
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableString* output =
[[[NSMutableString alloc] initWithString:@"batman!"] autorelease];
NSLog(@"output: %@", output);
[self performSelectorOnMainThread:@selector(FinishProcess)
withObject:nil waitUntilDone:false];
[pool drain];
}
对象 output
在自动释放池中被分配并设置为自动释放,将在函数结束前被释放。
- (void) FinishProcess
{
NSMutableString* output =
[[[NSMutableString alloc] initWithString:@"superman?"] autorelease];
NSLog(@"output: %@", output);
}
为主线程自动创建了一个自动释放池,所以在 FinishProcess
中,我们不需要创建自动释放池,因为这个函数在主线程上运行。
摘要
要避免 iPhone 应用中的内存泄漏,关键是要牢记每个已分配对象的归属者是谁、何时放弃该归属权,以及将 retain
和 release
调用成对使用。如果你遵循所有权规则,你的应用程序将更稳定,并将大大缩短调试时间。