要想用OC写出内存使用效率高而且又没有 bug 的代码,就得掌握其内存管理模型的种种细节。?一旦理解了这些规则,你就会发现,其实 Objective-C 的内存管理没那么复杂,而且有了"自动引用计数"(Automatic Reference Counting,ARC) 之后,就变得更为简单了。ARC 几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。
一 理解引用计数
OC语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或者递减的计数器。如果想使某个对象继续存活,那就增加其引用计数;用完了之后就递减其计数。计数变为0,就没人关注此对象了,于是就可以把它销毁。 从 Mac OS X 10.8 开始,“垃圾收集器”(garbage collector)已经正式废弃了,而iOS则从未支持过垃圾收集,将来也不会支持 已经用过ARC的人会知道:所有于引用计数有关的方法都无法编译。
引用计数工作原理
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在OC叫“保留计数”(retain count),不过也可以叫“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器:
retain :递增保留计数 release :递减保留计数 autorelease :待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。 查看保留计数的方法叫做retainCount,但这个方法不怎么有用,苹果公司并不推荐我们使用这个方法。 对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release或者autorelease 方法。最终当保留计数归零时,对象就回收了,也就是说,系统会将其占用的内存标记为“可重用”(reuse) 。此时,所有指向该对象的引用都变得无效了。 应用程序在其生命周期会创建很多对象,这些对象都互相联系着。可能互相引用,于是,这些相互关联的对象就构成了一张“对象图”(object graph)。对象如果持有指向其他对象的强引用(strong reference),那么前者就“拥有”(own)后者。也就是说,对象想令其所引用的那些对象继续存活,就可将其“保留”。等用完了之后再释放。 如下图,ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB与ObjectC继续存活,而应用程序里又有另一些对象想令那些对象继续存活。如果按照“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X程序中,此对象就是NSApplicaton对象;在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。 下面代码便于理解这个过程:
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
[array release];
如上所述,由于代码中直接调用了release方法,在ARC下无法遍译,调用alloc方法返回的对象由调用者所拥有,也就是说,调用者已经表达了令该对象存在的意愿,所以此刻保留计数至少为1. 创建完数组后,把number对象加入其中。调用数组的“addObject:”方法时,数组也会在number上调用retain方法,以继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要number对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number变量了。调用release后,已经无法保证所指的对象仍然存活,因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@"number = %@", number);
使上述代码在本例中可以正常执行,也仍然不是个好方法。如果调用release之后,基于某些原因,其保留计数降至0,那么number对象所占内存也许会回收,这样的话,再调用NSLog可能就使程序崩溃了。对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool)。如果执行NSLog时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的bug很难调试。 为避免在不经意间使用了无效对象,一般调用了release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬垂指针”(dangling pointer)。比方说,可以这样编写来防止其发生:
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
number = nil;
属性存取方法中的内存管理
如前所述,对象图由相互关联的对象所构成。刚才那个例子中的数组通过在其元素上调用retain方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”(strong relationship),则设置的属性值会保留。比如说,有个名叫foo的属性由名为_foo的实例变量所实现,那么,该属性的设置方法会是这样:
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要:假如还未保留新值就先把旧值释放了,而且两个值又指向同一对象,那么先执行的release操作就可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬垂指针。
自动释放池
在OC的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过也可能执行得更早些。 此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并不总是想令方法的调用者手工保留其值,比如:
- (NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return str;
}
此时返回的str对象其保留计数比期望值要多1(+1 retain count),因为调用alloc会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身一定是1,可能会大于1,这取决于“initWithFormat:”内部实现细节。要考虑的是如何将多出来的这一次保留操作抵消。 但是,不能在方法内释放str,否则还没等方法返回,系统就把该对象回收了。这里应该用autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会清空最外层的自动释放池时执行,除非有自己的释放池,否则这个时机指的就是当前进程下一次事件循环。改写方法,使用autorelease释放对象:
- (NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}
修改之后,stringValue方法把NSString对象返回给调用者时,对象必然存活。所以我们可以像下面这样使用:
NSString *str = [self stringValue];
NSLog(@"The string is: %@", str);
由于返回的str对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog语句在使用str对象前不需要手工执行保留操作。但是,假如要持有此对象的话(如将其设置给实例变量),那就需要保留,并于稍后释放:
_instanceVariable = [[self stringValue] retain];
[_instance release];
由此可见,autorelease能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
使用引用计数机制时,经常要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄露,因为循环中的对象其保留计数不会为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。如图,在这个循环里,所有对象的保留计数都是1。 在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全部都收走。而在OC的引用计数架构中,则享受不到这种便利。通常采用“弱引用”(weak reference)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,避免内存泄露。
以ARC简化引用计数
引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。Clang编译器项目带有一个“静态分析器”(static analyzer),用于指明程序里引用计数出问题的地方。 假设我们采用手动方式管理下面的代码:
if (1){
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
NSLog(@"str = %@", str);
}
上树代码存在内存泄漏,因为在if的语句块最后,我们没有释放str对象,由于在if外是没法使用str对象的,所以此对象所占的内存泄漏了。 判定内存泄漏规则:对象的保留计数比期望值多1。 静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或者释放操作以避免问题。自动引用计数这一思路正是源于此。自动引用计数所做的事与其名称相符,就是自动管理引用计数。于是,在前面那段代码的if语句块结束之前,可以于str对象上自动执行release 操作,也就是把代码自动改写为下列形式:
if (1){
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
NSLog(@"str = %@", str);
[str release];
}
使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在由ARC自动为你添加。除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。不过,ARC的那些功能都是基于核心的内存管理语义而构建成的,这套标准语义贯穿于整个OC语言。 由于ARC会自动执行管理引用计数操作,所以直接在ARC下调用这些方法是非法的,具体来说不能调用下面方法:
retain release autorelease dealloc 直接调用上述任何方法多会产生编译错误。此时必须信赖ARC,令其帮你正确处理内存管理。 实际上,ARC在调用这些方法的时候,并不通过普通的OC消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain 等价的底层函数_objc_retain 。这也是不能覆写retain、release、autorelease 的缘由,因为这些方法从来不会被直接调用。
使用ARC时必须遵循的方法命名规则
将内存管理语义在方法名中表示出来早已成为OC惯例,而ARC将之确立为硬性规定,这些规则简单的体现在方法名上,若方法名为以下词语开头,那么返回的对象归调用者所有。 alloc new copy mutableCopy 归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值 ,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease,那么保留计数的值可能比1大,这也就是retainCount方法不太有用的原因之一。 若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越其调用边界后依然有效。想要使对象多存活一段时间,必须令调用者保留它才行。 维系这些规则所需的全部内存管理事宜均由ARC自动管理,其中也包括在将要返回的对象上调用autorelease。
|