Effective Objective-C 2.0 第五章 内存管理 Item 29 理解引用计数

内存管理在任何面向对象的语言里,譬如 Objective-C(下文简称 OC),都是一个很重要的概念。理解任何一门语言的内存管理模型的详情,对写出节省内存、没有 bug 的代码,是非常重要的。
一旦你理解了这些规则, OC中的内存管理将不再复杂,随着自动引用计数 (ARC)的到来,这一切变得更加简单。ARC 几乎把所有内存管理的事宜都转移给编译器来决定,使得开发者只需专注于业务逻辑。

Item 29 理解引用计数

OC 使用引用计数进行内存管理,意味着每一个对象都有一个可增可减的计数器。当你对一个对象感兴趣,希望该对象存活,计数器+1,当你使用完该对象时,计数器-1。当对象的计数器降为 0 时,意味着该对象不再为任何对象所引用,可以被销毁了。这是一个简短的概述,如果想要写出好的 OC 代码,即使你正打算使用 ARC,理解什么是自动引用计数也是至关重要的。
OC 语音在 Mac OS X 上的垃圾回收机制在 Mac OS X 10.8 被官方禁用了,而 iOS 上从未有过 OC 语言的垃圾回收机制。理解引用计数在接下来是非常重要的,因为你不再能依靠 Mac OS X 的垃圾回收机制,而且你也从未、以后也不会在 iOS 上拥有垃圾回收机制。
如果你已经在使用 ARC,那么你应该忽略大部分在 ARC 的世界里代码无法编译的情况。因为这个 item 是从 OC 的角度出发,将在 ARC 下显示出来是非法的代码给展示出来,来解释引用计数。

引用计数是如何工作的

在引用计数的机制下,一个对象被分配了一个计数器,来指示有多少个事务对该对象感兴趣。这在 OC 里面被称为 retain count, 也被称为引用计数。以下三个方法声明自 NSObject protocol,用于维护计数器的递增、递减:

  • retain 增加 retain count。
  • release 减少 retain count。
  • autorelease 当自动释放池释放时,再减少 retain count。

方法 -retainCount可用于察看 retain count 的值,但即使在 debug 模式下也不怎么有用,苹果官方不鼓励使用。关于这个方法,下文还会有更详细的阐述。
对象被创建时,引用计数至少为 1。通过调用 -retain方法,注册对该对象的引用。当不再受到引用时,方法 -release·或-autorelease被调用。所有,当引用计数为 0 时,对象呗释放,意味着该对象的内存可回收并被重新使用。一旦对象被释放,所有对该对象的引用无效。
图1
图1. 一个对象在生命周期中引用计数增加和减少的过程
在一个应用的生命周期中,许多对象会被创造出来。这些对象一个与一个相关。例如,一个表示一个人的对象引用了表示这个人的名字的字符串对象,还会与其他的人(对象)产生关系,譬如这个人的朋友们,由此产生了一个对象图表。一个对象如果对另一个对象持有强引用,则称为拥有另一个对象。这意味着通过持有来表示对这些对象的生存的兴趣(keep them alive by retaining thme)。当使用完这些对象,他们将被释放。
在图 2 中的对象图表中,对象 A 同时被对象 B 和对象 C 所引用。当 B 和 C 同时使用完 A 时,A 的引用计数降为 0,A 可能会被销毁。B 和 C 也被其他对象所引用,被其他对象保存着存活。最终,如果你想追踪最后是谁持有着谁,你将得到一个根对象。在 Mac OS X 应用中,这个根对象可能是 NSApplication 对象;在 iOS 应用中,是 UIApplication 对象。他们都是应用启动后所创造的单例。
在这里插入图片描述
图2. 展示了一个对象在所有引用被释放后最终被销毁的对象图表
以下代码将帮助你在实践中理解这一切。

NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
// do something with 'array'
[array release];

如同前面所说的,这段代码在 ARC 下是无法通过编译,因为显式调用了 release。在 OC 中,对 alloc 的调用将导致返回一个属于调用者的对象。也即是说,调用者已经表达了对该对象的兴趣因为它使用了 alloc。然而这不是说引用计数就一定是 1,可能是更多,因为对 allocinitWithInt: 的执行,都有可能导致了其他的引用。能保证的就是引用计数至少是 1。你永远不能确切知道引用计数是多少,你所能知道的是你的行为会对引用计数产生什么影响:增加还是减少。
对象 number 添加进了数组中,在这个操作中,数组在方法 addObject: 中通过调用 retain 方法,表示了对 number 对象的引用关系。这样看, number 的引用计数至少为 2。然后, number 不再被这代码所需要,于是被释放,它的引用计数降为至少是 1。这时, number不再能够被安全的使用。当然,在这里它依然被数组所持有,在 release 之后依然存活。然而我们不能想当然的假设它还没被释放,这意味着我们不能像如下代码那样做:

NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@"number = %@", number);

即使这段代码走得通,也不建议这么做。如果出于各种可能的原因,在调用release 之后, 对象number的引用计数降为 0。最后一行代码将有崩溃的可能。我之所以说是"可能”是因为当一个对象被释放,它的内存回归了可重新被使用的内存池。如果这一块内存在NSLog调用时还没覆写,对象将有可能还存在着,此时将不会产生崩溃。由于这种原因,过早释放对象导致的bug经常很难被debug出来。

为了尽量避免不小心使用了不存在的对象,我们经常看到在release 之后将指针赋予 nil。这样将无法访问可能失效的对象了,我们称之为“野指针”。例如,像下面这样写:

NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;

属性访问器的内存管理
如前面所描述,对象图表中的对象相互之间连接在一起。上面例子中的数组通过引用来持有它的元素。同样的,其他对象通常是通过使用属性来持有其他对象,属性是使用访问器来读取和设置实例变量的。如果该属性是一个 strong 的引用关系,该属性的值将被持有。假设一个属性 foo (背后的实例变量是** _foo**),它的 setter 访问器如下:

-(void)setFoo:(id)foo {
    [foo retain];
    [_foo release];
    _foo = foo;
}

新值被 retain,旧值被 release,然后实例变量指向新值。这个顺序很重要,如果旧值在新值被 retain 之前被 release,而新旧值又刚好是同一个,那就有可能导致对象被过早销毁,接下来的 retain 也不能复活被销毁的对象,那么这个实例变量将变成野指针。

自动释放池
OC 的引用计数机制的一个重要的特性,就是自动释放池。与调用 release 将引用计数立马 -1 不同,你也可以调用 autorelease,它会在之后的某个时间在执行 release,通常是在下一个事件 loop 结束之后,也可能很快。
这个特性非常有用,尤其从方法返回一个对象作为返回值时。这种情况下,你不希望调用者持有这个返回值。譬如如下代码:

-(NSString *)stringValue {
    NSString *str = [NSString alloc] initWithFormat:@"I am this:%@", self];
    return str;
}

这个例子中,str 返回时由于调用了 alloc,引用计数是 a+1,且没有与之相对应的 release。这个 +1 意味着作为调用者你需要在某处对这个对象进行 release。这个对象的引用计数不一定刚好是 1,但那些细节是在方法 -initWithFormat: 中的,你所需要操心的是平衡这个 +1。
但是你不能再方法里面对 str 进行 release,因为这有可能导致 str 在返回之前被销毁。所以 autorelease 用来表示这个对象应该在之后被销毁,但必须确保存活的时间足够久,使得调用者如果需要持有时有足够的时间去 retain 它。换句话说,对象在方法的边界被保证存活。事实上,但最外层的自动释放池释放时,对象将会被释放,除非你有自己的自动释放池,在下一次围绕当前线程是事件循环时。(In fact, the release will happen when the outermost autorelease pool is drained, which, unless you have your own autorelease pools, will be next time around the current thread’s event loop.)应用到上面这个例子,如下:

-(NSString *)stringValue {
    NSString *str = [NSString alloc] initWithFormat:@"I am this:%@", self];
    return [str autorelease];
}

现在这个 NSString 对象在返回时肯定是存活的,可如下使用:

NSString *str = [self stringValue];
NSLog(@"The string is:%@", str);

这里不需要更多的内存管理,因为 str 对象返回之后被 autorelease。由于自动释放池里的对象在下一个事件循环开始之前不会被释放,对象在被打印的时候,不需要显式地 retain。如果对象需要被持有,譬如被赋予某个实例变量,那么就需要进行 retain 并随后进行 release:

_instanceVariable = [[self stringValue] retain];
// ...
[_instanceVariable release];

autorelease 是延长对象生命周期的方法,使得它在方法的调用边界上存活(so that is can survive across method call boundaries)。

循环引用
引用计数一个需要被警惕的常见的现象就是循环引用。循环引用,发生在一个对象与另一个对象相互引用时。由于此时在这个循环里面,没有一个对象的引用计数会降为 0,这将导致内存泄漏。如下图,三个对象都对另外两个对象中的一个有着持有关系,在这个循环里面,所有对象的引用计数均为 1。
在这里插入图片描述
在垃圾回收环境下,这种情况将作为隔离孤岛被处理掉,回收器会销毁这三个对象。在 OC 的引用计数机制下,这是不可能的奢望。我们需要通过弱引用来解决这个问题,或者通过外部的影响,使其中一个对象放弃它对另一个对象的持有。这样一来,引用循环就会被打破,内存就不会泄漏了。

小结

  • 引用计数的内存管理机制是基于一个可增可减的计数器。对象被创建时引用计数至少是 1。引用计数大于 0 时对象存活,降为 0 时对象被销毁。
  • 在一个对象的生命周期中,它会被持有它的对象 retain 或 release。retain 和 release 分别对计数器产生 +1 和 -1 的影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值