从 C++ 到 Objective-C(15):内存管理(续二)

Setters

如果不对 Objective-C 的内存管理机制有深刻的理解,是很难写出争取的 setter 的。假设一个类有一个名为 title 的NSString类型的属性,我们希望通过 setter 设置其值。这个例子虽然简单,但已经表现出 setter 所带来的主要问题:参数如何使用?不同于 C++,在 Objective-C 中,对象只能用指针引用,因此 setter 虽然只有一种原型,但是却可 以有很多种实现:可以直接指定,可以使用retain指定,或者使用copy。每一种实现都有特定的目的,需要考虑你 set 新的值之后,新值和旧值之间的关系(是否相互影响等)。另外,每一种实现都要求及时释放旧的资源,以避免内存泄露。

直接指定(不完整的代码)

外面传进来的对象仅仅使用引用,不带有retain。如果外部对象改变了,当前类也会知道。也就是说,如果外部对象被释放掉,而当前类在使用时没有检查是否为nil,那么当前类就会持有一个非法引用。

-(void) setString:(NSString*)newString
{
    ... 稍后解释内存方面的细节
    self->string = newString; // 直接指定
}

使用retain指定(不完整的代码)

外部对象被引用,并且使用retain将其引用计数器加 1。外部对象的改变对于当前类也是可见的,不过,外部对象不能被释放,因为当前类始终持有一个引用。

-(void) setString:(NSString*)newString
{
    ... 稍后解释内存方面的细节
    self-> string = [newString retain]; // 使用 retain 指定
}

复制(不完整的代码)

外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆, 这个对象的生命周期不会比持有者更长。

-(void) setString:(NSString*)newString
{
    ... 稍后解释内存方面的细节
    self->string = [newString copy]; // 克隆
    // 使用 NSCopying 协议
}

为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter 都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。

直接指定( 完整代码)

这是最简单的情况。旧的引用实际上被替换成了新的。

-(void) setString:(NSString*)newString
{
    // 没有强链接,旧值被改变了
    self->string = newString; // 直接指定
}

使用retain指定(完整代码)

在这种情况下,旧值需要被释放,除非旧值和新值是一样的。

// ------ 不正确的实现 ------
-(void) setString:(NSString*)newString
{
    self->string = [newString retain];
    // 错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放
}

-(void) setString:(NSString*)newString
{
    [self->string release];
    self->string = [newString retain];
    // 错误!如果 newString == string(这是可能的),
    // newString 引用是 1,那么在 [self->string release] 之后
    // 使用 newString 就是非法的,因为此时对象已经被释放
}

-(void) setString:(NSString*)newString
{
    if (self->string != newString)
        [self->string release]; // 正确:给 nil 发送 release 是安全的
    self->string = [newString retain];  // 错误!应该在 if 里面
                                        // 因为如果 string == newString,
                                        // 计数器不会被增加
}

// ------ 正确的实现 ------
// 最佳实践:C++ 程序员一般都会“改变前检查”
-(void) setString:(NSString*)newString
{
    // 仅在必要时修改
    if (self->string != newString) {
        [self->string release]; // 释放旧的
        self->string = [newString retain]; // retain 新的
    }
}

// 最佳实践:自动释放旧值
-(void) setString:(NSString*)newString
{
    [self->string autorelease]; // 即使 string == newString 也没有关系,
                                // 因为 release 是被推迟的
    self->string = [newString retain];
    //... 因此这个 retain 要在 release 之前发生
}

// 最佳实践:先 retain 在 release
-(void) setString:(NSString*)newString
{
    [self->newString retain]; // 引用计数器加 1(除了 nil)
    [self->string release]; // release 时不会是 0
    self->string = newString; // 这里就不应该再加 retain 了
}

复制(完整代码)

无论是典型的误用还是正确的解决方案,都和前面使用retain指定一样,只不过把retain换成copy

伪克隆

有些克隆是伪克隆,不过对结果没有影响。

3 Comments

  1. willonboy's blog 2011年4月4日
    • DevBean 2011年4月5日
      • Rex 2011年9月26日

Leave a Reply