内存管理原则

大家好,本文是文档翻译计划的第 02 篇,原文是:Memory Management Policy

一组定义在 NSObject protocol 的方法,以及一种标准方法命名约定,提供了在引用计数环境下的内存管理的基本模型。NSObject 这个类定义了名为 dealloc 的方法,当对象被释放的时候会被调用。本文介绍了在 Cocoa 项目中正确管理内存的一些基本规则,并提供一些正确的使用例子。

内存管理基本规则

内存管理基于对一个对象的所有权,一个对象可以被多个对象引用。只要一个对象被引用着,就会一直存在不被释放;如果一个对象不再被引用,则会被系统自动回收释放。为了确保引用关系更加简单明确,Cocoa 规定了一些原则:

  • 对自己创建的对象有引用
    通过以方法名以 “alloc”, “new”, “copy”, or “mutableCopy” 开头的方法创建的对象。

  • 通过 retain 可以引用一个对象
    以函数参数形式传入的对象,在函数执行完毕之前都不会被销毁,该函数也可以放心的将该对象作为结果返回给调用者。
    你可以在下面两个场景下使用 retain:① 在 getter 方法中或者 init 方法中使用 retain 来持有一个对象。② 防止其他操作的副作用引起正在使用的对象被释放,具体参考:Avoid Causing Deallocation of Objects You’re Using

  • 当你不再需要某个对象的时候,需要放弃对其的引用
    你可以使用 release 或 autorelease 来放弃对一个对象的引用。在 Cocoa 术语中,引用通常被称为 “releasing” 对象。

  • 不要放弃一个你没拥有的对象
    这只是上面明确提到的规则的推论。

[译者注] 内存管理的原则,一句话概括就是「谁创建的,谁释放」。这种规则最简单,谁让对象的引用计数加一,谁就负责在不再使用该对象的时候把引用计数减一。其中,引用计数加一用 retain,引用计数减一用 release 或者 autorelease。

一个简单的例子

为了说明上面提到的规则,请看下面这个例子。

{
    Person *aPerson = [[Person alloc] init];
    // ...
    NSString *name = aPerson.fullName;
    // ...
    [aPerson release];
}

aPerson 这个对象创建的时候用的 alloc,所以后面在不在用到这个对象的时候调用了 release。name 这个对象调用的方法不是以 alloc 等关键字开头,所以不持有,所以不需要调用 release。

使用 autorelease 发送延迟的 release 操作

使用 autorelease 发送延迟的 release 操作,特别是在函数中返回一个对象的时候。比如你可以这么来实现 fullname 函数。

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName] autorelease];
    return string;
}

你通过 alloc 创建一个对象,所以你有它的引用。为了遵循内存管理原则,你需要在不再需要改对象时释放对该对象的引用。如果你使用 release,则这个字符串在返回给调用者之前就已经被销毁了(这个函数返回了一个非法的对象)。使用 autorelease,你标明你想释放对这个字符串的引用,但是是延迟释放,这样的话调用者就可以拿到该对象了。

[译者注] autorelease 依赖自动释放池,调用了 autorelease,则该对象的引用计数不会立刻减一,而是等到代码所在的最近的一个自动释放池结束的时候才会被减一,进而对象才会被释放。关于自动释放池的内容,后面也会有相关的文章。

你也可以这么实现 fullname 方法。

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@",
                                 self.firstName, self.lastName];
    return string;
}

根据上面提到的基本规则,创建 string 这个对象的时候,用到的函数不是以 alloc 等开头,所以你并不拥有对改对象的引用,所以直接返回对象即可。

作为对比,下面的实现是错误的。

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
                                         self.firstName, self.lastName];
    return string;
}

根据命名规则,调用者并没有拥有对函数返回的字符串的引用,所以调用者也不会 release 改字符串,因此该字符串对象泄漏了。

你不持有通过引用返回的对象

在 Cocoa 的一些方法中,指定通过引用返回一个对象(参数是 ClassName ** 或者 id *)。常见的模式是使用NSError对象,该对象包含发生错误时有关错误的信息。

当你执行这些方法时,你并没有拥有通过引用返回的对象,所以也不需要调用 release 释放它。用下面这个例子说明一下:

NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
                        encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
    // Deal with error...
}
// ...
[string release];

[译者注] 通过二级指针来获取一个对象,在 Cocoa 中很多地方有这种用法,最经典的就是上面例子中的 initWithContentsOfFile,通过二级指针很方便的取回一个对象。但是这种获得对象的方式并不符合上面提到的引用计数相关的原则,所以这里其实是增加了一种规则。

在 dealloc 中释放对对象的引用

NSObject 类定义了 dealloc 方法,当一个对象的引用计数为 0 的时候并且内存被回收时,会被调用 dealloc 方法。dealloc 的作用是释放自己的内存、丢弃对其他对象或资源的引用。

下面的例子说明了你应该如何实现 Person 这个类的 dealloc 方法:

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
 
@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}
@end

Important:

  1. 永远不要直接调用 dealloc 函数。
  2. 记得在 dealloc 的结尾调用 [super dealloc]。
  3. 你不应该将系统资源的管理与对象生存期联系起来,参考:Don’t Use dealloc to Manage Scarce Resources
  4. 当应用终止的时候,对象的 dealloc 方法不会被调用。因为应用进程的内存会被自动清理,操作系统直接清理要比逐个调用众多对象的 dealloc 方法要高效的多。

Core Foundation 使用类似但是不完全一样的原则来管理内存

Core Foundation 对象的内存管理规则也有很多相似之处(参考:Memory Management Programming Guide for Core Foundation)。但是 Cocoa 和 Core Foundation 的命名约定不一样。特别是创建对象的规则(参考:The Create Rule)不适用于返回 Objective-C 对象的方法。例如下面的代码片段,你不需要考虑 myInstance 的持有和释放问题。

MyClass *myInstance = [MyClass createInstance];