第29条:理解引用计数
第5章 内存管理
在 Objective-C 这种面向对象语言里,内存管理是个重要概念。要想一门语言写出内存使用效率高而且又没有 bug 的代码,就得掌握其内存管理模型的种种细节。
一旦理解了这些规则,你就会发现,其实 Objective-C 的内存管理没那么复杂,而且有了 “自动引用计数”(Automatic Reference Counting, ARC)之后,就变得更为简单了。ARC 几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。
本条要点:(作者总结)
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为 1。若保留计数为正,则对象继续存活。当保留计数降为 0 时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放对象。保留于释放操作分别会递增及递减保留计数。
Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数:用完了之后,就递减其计数。计数变为 0,就表示没人关注此对象了,于是,就可以把它销毁。上面这几句话只是个概述,要想写出优秀的 Objective-C 代码,必须完全理解此问题才行,即便打算用 ARC 来编码也是如此。
从 Mac OS X 10.8 开始,“垃圾收集器”(garbage collector)已经正式废弃了,以 Objective-C 代码编写 Mac OS X 程序时不应再使用它,而 iOS 则从未支持过垃圾收集。因此,掌握引用计数机制对于学好 Objective-C 来说十分重要。Mac OS X 程序已经不能再依赖垃圾收集器了,而 iOS 系统不支持此功能,将来也不会支持。
已经用过 ARC 的人可能会知道:所有与引用计数有关的方法都无法编译,然而现在先暂时忘掉这件事。那些方法确实无法用在 ARC 中,不过本条就是要从 Objective-C 的角度讲解引用计数,而 ARC 实际上也是一种引用计数机制,所以,还是要谈谈这些在开启 ARC 功能时不能直接调用的方法。
引用计数工作原理
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做 “保留计数” (retain count),不过也可以叫 “引用计数”(reference count)。NSObject 协议声明了下面三个方法用于操作计数器,以递增或递减其值:
- Retain 递增保留计数。
- release 递减保留计数。
- autorelease 待稍后清理 “自动释放池”(autorelease pool)时,再递减保留计数。(本条后面几页与本书第 34 条将会详细讲解 “自动释放池”。)
查看保留计数的方法叫做 retainCount,此方法不太有用,即便在调试时也如此,所以笔者(与苹果公司)并不推荐大家使用这个方法。更多内容请参阅第 36 条。
对象创建出来时,其保留计数至少为 1。若想令其继续存活,则调用 retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用 release 或 autorelease 方法。最终当保留计数归零时,对象就回收了(deallocated),也就是说系统会将其占用的内存标记为 “可重用”(reuse)。此时,所有指向该对象的引用也都变得无敌了。
图演示了对象自创造出来之后历经一次 “保留” 及两次 “释放” 操作的过程。
应用程序在其生命周期中会创建很多对象,这些对象都相互联系着。例如,表示个人信息的对象会引用另一个表示人名的字符串对象,而且可能还会引用其他个人信息对象,比如在存放朋友的 set 中就是如此,于是,这些相互关联的对象就构成了一张 “对象图”(object graph)。对象如果持有指向其他对象的强引用(strong reference),那么前者就 “拥有” (own)后者。也就是说,对象想令其所引用的那些对象继续存活,就可将其 “保留”。等用完了之后,再释放。
在图所示的对象图中,ObjectB 与 ObjectC 都引用了 ObjectA。若 ObjectB 与 ObjectC 都不再使用 ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令 ObjectB 与 ObjectC 继续存活,而应用程序里又有另外一些对象想令那些对象继续存活,如果按 “引用树” 回溯,那么最终会发现一个 “根对象”(root object)。在 Mac OS X 应用程序中,此对象就是 NSApplication 对象;而在 iOS 应用程序中,则是 UIApplication 对象。两者都是应用程序启动时创建的单例。
下面这段代码有助于理解这些方法的用法:
1 NSMutableArray *array = [[NSMutableArray alloc] init]; 2 3 NSNumber *number = [[NSNumber alloc] initWithInt:1337]; 4 5 [array addObject:number]; 6 7 [number release]; 8 9 // do something with 'array' 10 11 [array release];
ObjectC 释放了 ObjectA ObjectB 释放了 ObjectA
如前所述,由于代码中直接调用了 release 方法,所以在 ARC 下无法编译。在 Objective-C 中,调用 alloc 方法所返回的对象由调用者所拥有。也就是说,调用者已通过 alloc 方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在 alloc 或 "initWithInt:" 方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于 1。能够肯定的是:保留计数至少为 1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。
创建完数组后,把 number 对象加入其中。调用数组的 "addObject:" 方法时,数组也会在 number 上调用retain 方法,以期继续保留此对象。这时,保留计数至少为 2。接下来,代码不再需要 number 对象了,于是将其释放。现在的保留计数至少为 1。这样就不能照常使用 number 变量了,于是将其释放。现在的保留计数至少为 1。这样就不能照常使用 number 变量了。调用 release 之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道 number 对象在调用了 release 之后仍然存活,因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码:
1 NSNumber *number = [[NSNumber alloc] initWithInt:1337]; 2 3 [array addObject:number]; 4 5 [number release]; 6 7 NSLog(@"number = %@", number);
即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用 release 之后,基于某些原因,其保留计数降至 0 ,那么 number 对象所占内存也许会回收,这样的话,再调用 NSLog 可能就将使程序奔溃了。笔者在这里只说 “可能”, 而没说 “一定”,因为对象所占的内存在 “解除分配”(deallocated)之后,只是放回 “可用内存池”(avaiable pool)。如果执行 NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的 bug 很难调试。
为避免在不经意间使用了无效对象,一般调用完 release 之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为 “悬挂指针”(dangling pointer)(亦称“迷途指针”、“悬挂指针”、“悬摆指针”)。比方说,可以这样编写代码来防止此情况发生:
1 NSNumber *number = [[NSNumber alloc] initWithInt:1337]; 2 3 [array addobject:number]; 4 5 [number release]; 6 7 number = nil;
属性存取方法中的内存管理
如前所述,对象图由互相关联的对象所构成。刚才那个例子中的数组通过在其元素上调用 retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问 “属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为 “strong 关系”(strong relationship),则设置的属性值会保留。比方说,有个名叫 foo 的属性由名为 _foo 的实例变量所实现,那么,该属性的设置方法会是这样:
1 - (void)setFoo:(id)foo { 2 3 [foo retain]; 4 5 [_foo release]; 6 7 _foo = foo; 8 9 }
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的 release 操作就可能导致系统将此对象永久回收。而后续的 retain 操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针了。
自动释放池
在 Objective-C 的引用计数架构中,自动释放池是一项重要特性。调用 release 会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用 autorelease,此方法会在稍后递减计数,通常是在下一次 “事件循环”(event loop)时递减,不过也可能执行得更早些。
此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并不总是想令方法调用者手工保留其值。比方说,有下面这个方法:
1 - (NSString *)stringValue { 2 3 NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self]; 4 5 return str; 6 7 }
此时返回的 str 对象其保留计数比期望值要多 1 (+1 retain count),因为调用 alloc 会令保留计数加 1,而又没有与之对应的释放操作。保留计数多 1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身就一定是 1,它可能大于 1,不过那取决于 “initWithFormat:” 方法内的实现细节。你要考虑的是如何将多出来的这一次保留操作抵消掉。
但是,不能在方法内释放 str,否则还没等方法返回,系统就把该对象回收了。这里应该用 autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越 “方法调用边界”(method callboundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池(参见第 34 条)时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写 stringValue 方法,使用 autorelease 来释放对象:
1 - (NSString *)stringValue { 2 3 NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self]; 4 5 return [str autorelease]; 6 7 }
修改之后,stringValue 方法把 NSString 对象返回给调用者时,此对象必然存活。所以我们能够像下面这样使用它:
NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);
由于返回的 str 对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以 NSLog 语句在使用 str 对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放:
1 _instanceVariable = [[self stringValue] retain]; 2 3 // ... 4 5 [_instanceVariable release];
由此可见, autorelease 能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
使用引用计数机制时,经常要注意的一个问题就是 “保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏,因为循环中的对象其保留计数不会降为 0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。图里的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是 1。
在垃圾收集环境中,通常将这种情况认定为 “孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全都回收走。而在 Objective-C 的引用计数架构中,则享受不到这一便利。通常采用 “弱引用”(weak reference,参见第 33 条)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄漏。
END