2-4.【OC】【内存管理】MRC 下,下面代码是否安全?为什么?

3 阅读3分钟
@property (nonatomic, retain) NSObject *obj;
_obj = [[NSObject alloc] init];
self.obj = _obj;
[_obj release];

在 MRC(Manual Reference Counting)环境下,这段代码是安全的,但它代表了一种极度不规范且容易埋坑的写法。

虽然最终内存引用计数是平衡的,但它违反了 Objective-C 的“所有权语义”一致性,在更复杂的场景下会导致崩溃。


1. 内存走线分析

我们来拆解这三行代码执行后,引用计数(Reference Count, RC)的变化:

  1. _obj = [[NSObject alloc] init];

    • alloc 使对象 RC = 1
    • 根据规则,你(当前作用域)拥有这个对象的所有权。
  2. self.obj = _obj;

    • 调用了 setter 方法。由于属性标记为 retainsetter 内部会执行 [_obj retain]
    • 此时对象 RC = 2
    • 现在有两个持有者:你的局部变量 _obj 和类的成员变量 _obj
  3. [_obj release];

    • 局部变量放弃所有权,对象 RC = 1
    • 此时对象依然存活,被 self.obj 妥善持有。

结论: 内存没有泄露,也没有野指针,逻辑上是安全的。


2. 为什么这种写法“不专业”?

在工程实践中,这种“先 alloc 给成员变量,再通过 self 赋值,最后手动 release”的操作非常繁琐且冗余。它存在以下两个问题:

A. 语义重复(Double Ownership)

你通过 alloc 拿到了一次所有权,又通过 setter 拿到了第二次所有权,最后再放掉第一次。这像是在搬家时,你自己抱起一个箱子(alloc),把它交给搬家公司(setter),然后你再把手松开(release)。

B. 潜在的 Setter 陷阱

如果在 init 方法里这样做,会有风险。在 initdealloc 中,苹果官方建议直接访问成员变量(即 _obj = ...),而不是通过属性(self.obj = ...)。

  • 原因setter 可能被子类重写。如果子类在重写的方法里访问了尚未初始化完成的其他属性,程序会直接崩溃。

3. 更正宗(Idiomatic)的两种写法

写法一:直接赋值(最常用)

如果你是在 init 方法里:

Objective-C

// 只有一次 alloc,RC = 1
_obj = [[NSObject alloc] init]; 

直接操作底层变量,不触发 setter,既高效又安全。

写法二:利用 Autorelease(工厂方法风格)

如果你非要用 self.obj 赋值:

Objective-C

// 使用类方法,返回的对象是 autorelease 的,你不拥有它
self.obj = [NSObject new]; // 注意:在 MRC 中 new 依然需要 release,通常改用:
self.obj = [[[NSObject alloc] init] autorelease]; 
  • alloc 产生 RC=1,autorelease 标记延迟释放。
  • self.obj 执行 retain 使 RC=2。
  • 当前周期结束,池子发 release 使 RC=1。
  • 优点:代码只有一行,逻辑清晰。

4. 延伸:Setter 内部发生了什么?

为了确保安全,属性声明为 retain 时,编译器(或你手写的)setter 必须处理“自赋值”情况:

Objective-C

- (void)setObj:(NSObject *)newObj {
    if (_obj != newObj) {
        [_obj release];    // 释放旧的
        _obj = [newObj retain]; // 持有新的
    }
}

总结

你提供的代码在内存引用平衡上是闭环的,但它属于**“绕路走”**。在 MRC 时代,这种写法增加了代码行数,也就增加了手抖写错 release 的概率。