Matrixzk’s Blog

keep moving

OC内存管理那些事儿(1):内存管理策略

Nov 9th, 2014

在引用计数环境下用于内存管理的基本模型,是由 NSObject protocol中所定义的一些方法,结合一套标准的方法命名约定共同提供的。NSObject 类还定义了一个dealloc方法,该方法会在对象被释放时自动调用。本文将讲解在 Cocoa 编程中所需了解的关于正确进行内存管理的基本规则,并提供一些正确用法的示例。

内存管理的基本规则

对象管理模型基于对象所有权。每个对象都有一或多个持有者(owner)。只要某对象还有一个持有者,它就不会被释放掉。如果一个对象没有了持有者,运行时系统(runtime system)就会自动将其销毁掉。为了确保让你清晰地意识到何时持有了一个对象,Cocoa 采用了如下策略:

  • 只要是你创建的对象,你就拥有其所有权
    使用名字以alloc, new, copy 或者 mutableCopy 开头的方法所创建对象(比如,alloc, newObject, 或者mutableCopy)。

  • 使用retain来获取对象的所有权
    一个获取到的对象通常要确保它在被接收的方法内是始终有效的,并且该方法也要确保安全得将该对象返回给它的调用者。使用retain有如下两个场景:
    (1) 在存取器(accessor)方法或init方法的实现中,拿到一个想要存储为属性值的对象的所有权;
    (2) 防止一个对象因其他操作的副作用而变成无效的(就像在 要避免对象在使用过程中被释放 中所阐述的那样)。

  • 当不再需要一个对象时,必须要放弃对它的所有权
    通过给一个对象发送release消息或者autorelease消息来放弃持有它。在 Cocoa 的惯用语中,放弃对一个对象的所有权通常被称为释放(release)一个对象。

  • 如果不是某对象的持有者,不要执行放弃其所有权的操作
    这只是前边几条策略规则的一个推论,在这里明确阐述下。

一个简单的例子

为了举例说明上述策略,思考如下代码片段:

1
2
3
4
5
6
7
{
    Person *aPerson = [[Person alloc] init];
    // ...
    NSString *name = aPerson.fullName;
    // ...
    [aPerson release];
}

对象Person是由alloc方法创建的,所以随后在不再需要它时向它发送了release消息。这里的name不是通过附带所有权的方法(the owning methods)取到的,所以不必向其发送release消息。值得注意的是,本例中使用了release而不是autorelease

使用 autorelease 延迟发送 release 消息

当需要延迟发送release消息时,使用autorelease,特别是在要从一个方法中返回一个对象时。比如,可以像这样实现上例中的fullName方法:

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

你将持有这里返回的通过alloc创建的string。按照内存管理的规则,在你丢掉对string的引用之前,必须先放弃对它的所有权。然而,这里如果使用releasestring将会在被返回前就被释放掉(该方法将返回一个无效的对象)。使用autorelease,表示你想要放弃其所有权,但允许该方法的调用者在这里返回的string被释放前使用它。

fullName方法的实现也可以像这样:

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

根据上述的基本规则,这里你会持有通过stringWithFormat:返回的string,所以可以放心得从该方法中返回string

作为对比,如下实现是错误的:

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

依据上述命名约定,fullName方法的调用者并不认为自己持有了这里所返回的string,因此它也就没有理由释放所返回的string,于是,这里就导致了内存泄漏。

你不具有通过引用(reference)返回的对象的所有权

Cocoa 中的某些方法会指定通过引用(reference)返回一个对象(也就是说,它们接受一个 ClassName **id *类型的参数)。一个常见的例子是,使用NSError对象来包含某个可能出现的 error 的相关信息。比如,initWithContentsOfURL:options:error: (NSData) 和 initWithContentsOfFile:encoding:error: (NSString)。

前边所述的规则同样适用于这种情况。当调用上述方法时,由于你并没有创建NSError对象,也就不具有其所有权,因此也就没义务释放它,就像这个例子:

1
2
3
4
5
6
7
8
9
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];

实现 dealloc 方法以释放所持有的对象

NSObject类中有一个dealloc方法,它将在一个对象没有任何持有者时被自动调用,该对象所占内存也将被回收 – 在 Cocoa 的惯用语中这被称为freed或者deallocated(释放)。dealloc方法所扮演的角色是,释放对象所占用的内存,处理它所占用的资源,包括对象的所有实例变量的所有权。

下面以Person类为例来说明下其dealloc方法的一种实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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

重要:
(1) 千万不要直接调用另一个类的dealloc方法;
(2) 必须在所实现的dealloc方法的末尾调用父类(superclass)的dealloc方法;
(3) 不要把对系统资源的管理纳入对象的生命周期 (详情参考 不要在 dealloc 方法中管理稀缺资源 );
(4) 当应用终止后,对象的dealloc方法可能并没有被调用。由于程序所占用的内存会在其终止时被自动清理掉,所以只需让操作系统去清理其所占用的资源就行,这会比挨个儿调用那些内存管理方法更高效。

Core Foundation 使用类似的规则

Core Foundation 对象的内存管理规则与此类似 (详情参阅 Memory Management Programming Guide for Core Foundation )。然而,Cocoa 与 Core Foundation 的命名约定有所不同。需要注意,Core Foundation 的创建规则 (参考 The Create Rule )并不适用于返回 Objective-C 对象的方法。例如,在如下所示的代码片段中,这里不用负责释放myInstance的内存:

1
MyClass *myInstance = [MyClass createInstance];
返回顶部