Objective-C语言以引用计数来管理内存,这令许多初学者纠结,要是用过以“垃圾收集器”(garbage collector)来管理内存的语言,那么更会如此。“自动引用计数”机制缓解了此问题,不过使用时有很多重要的注意事项,以确保对象模型正确,不致内存泄漏。本章提醒读者注意内存管理中易犯的错误。
从Mac OS X 10.8开始,垃圾收集器已经正式废弃,而iOS则从未支持过垃圾收集。因此,掌握引用计数机制对于学好Objective-C十分重要。
在引用计数架构下,对象有个计数器,表示当前有多少对象想令该对象继续存活,在Objective-C中叫做“保留计数”(retain count)或“引用计数”(reference count)。NSObject协议声明了三个方法用于操作计数器,以递增/递减其值:
retain
:递增保留计数。
release
:递减保留计数。
autorelease
:待稍后清理自动释放池(autorelease pool)时,再递减保留计数。
查看保留计数的方法是retainCount
,此方法不太有用,即便在调试时也是如此,故笔者(与苹果公司)并不推荐使用这个方法。
为避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能确保不会出现可能指向无效对象的指针,这种指针叫做“悬挂指针”(dangling pointer)。例如,可以这样编写代码以防止此情况发生:
NSNumber *number = [[NSNumber alloc] initWithInt: 1337];
[array addObject:number];
[number release];
number = nil;
对象图是由相互关联的对象所构成。不光是数组,其他对象也可以保留别的对象,这一般通过访问属性来实现。访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”(strong relationship),则设置的属性值会保留。例如:
- (void)setFoo:(id)foo{ [foo retain]; [_foo release]; _foo = foo; }
在以上代码中,请注意先保留新值再释放旧值的顺序。
autorelease
方法能够延长对象生命期,使其在跨越方法调用边界(method call boundary)后依然可以存活一段时间。它会在稍后递减保留计数,通常是在下一次“事件循环”(event loop)时递减,不过也可以通过@autoreleasepool
让这个过程执行得更早。
使用引用计数机制时,经常需要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄漏。
在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时垃圾收集器会把这三个对象全部回收走。而在Objective-C中,由于没有垃圾收集器,通常采用“弱引用”(weak reference)来解决此问题,或是从外界命令引用循环中的某个对象不再保留另外一个对象来打破这个保留环,从而避免内存泄漏。
引用计数这个概念相当容易理解,需要执行保留及释放操作的地方也很容易就能看出来。故Clang编译器项目带有一个“静态分析器”(static analyzer),用于指明程序里引用计数出问题的地方。静态分析器还有更为深入的用途,可以根据需要预先加入
恰当的保留或释放操作以避免这些问题,即自动引用计数。
由于ARC会自动执行retain
、release
、autorelease
等操作,故直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:retain
、release
、autorelease
、dealloc
。直接调用上述任何方法都会产生编译错误,因为ARC要分析何处应该自动调用内存管理方法。
实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层的C语言版本,以提升性能。例如,ARC会调用与retain等价的底层函数objc_retain。这也是不能覆写retain
、release
或autorelease
的缘由,因为这些方法在ARC下从来不会被直接调用。
将内存管理语义在方法名种表示出来早已是Objective-C的惯例,而ARC则将之确立为硬性规定。若方法名以alloc,new,copy,mutableCopy这些词语开头,则其返回的对象归调用者所有,即调用这些方法的那段代码要负责释放返回的对象。
若方法名不以上述四个词语开头,则表示其返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
除了会自动调用“保留”与“释放”方法之外,使用ARC还有其他好处,可以执行一些手动操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够相互抵消的retain、release、autorelease操作约简。这也是之所以建议大家在以后编码中都应该用ARC的原因。
例如autorelease及紧跟其后的retain操作,为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数objc_autoreleaseReturnValue,而不直接调用对象的autorelease方法,此函数会检查当前方法返回后即将执行的那段代码。若发现将要执行retain操作,则设置全局数据结构(因处理器而异)中的一个标志位,而不执行autorelease操作;之后也不直接执行retain,而是改为执行objc_retainAutoreleasedReturnValue函数。此函数检查上述提到的标志位,若已经置位则不执行retain操作。设置并检测标志位,要比调用autorelease和retain更快。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong:默认语义,保留其值。
__unsafe_unretained:不保留其值,这么做可能不安全,因为等到再次使用该变量时,对象可能已经被回收了。
凡是强引用的变量都必须释放,ARC会再自动生成的dealloc方法中插入这些代码。
回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构函数。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。ARC借助此特性,在.cxx_destruct方法中生成清理内存代码。
不过,如果有非Objective-C的对象,比例CoreFoundation中的对象(需要调用CFRetain/CFRelease)或是由malloc分配在堆中的内存,仍然需要在dealloc方法中显式清理。然而不需要像原来那样调用超类的dealloc方法,因为ARC会自动在.cxx_destruct方法中调用超类的dealloc方法。
不使用ARC时,可以覆写内存管理方法。比如,在实现单例类时,因为单例不可释放,所以我们经常覆写release方法,将其替换为“空操作”(no-op)。
但在ARC环境下不能这样做,会干扰到ARC分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以ARC能够优化retain、release、autorelease操作,使之不经过Objective-C的消息派发机制。优化后的操作,直接调用隐藏在运行期程序库中的C函数。
对象在经历其生命期后,最终为系统所回收,这时运行期系统就要执行dealloc方法了。具体何时执行则无法保证,而且你决不应该自己调用dealloc方法。一旦运行期系统调用dealloc方法后,对象就不再有效了。
在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或NSNotificationCenter等通知,不要做其他事情。
如果对象持有文件描述符等系统资源,应该专门编写一个方法来释放此资源,例如称作close方法,而且在用完资源后必须调用close方法。
执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc方法里调用,因为此时对象已处于正在回收的状态了。
需要注意的是,在dealloc方法里不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于键值观测机制的监控下,该属性的观察者可能会在属性改变时保留或使用这个即将回收的对象。这会令运行期系统的状态完全失调,导致莫名其妙的错误。
纯C中没有“异常”(exception),而C++与Objective-C都支持“异常”。实际上,在当前的运行期系统中,C++与Objective-C的异常相互见人。也就是说,从其中一门语言里抛出的异常能用另一种语言所编的“异常处理程序”(exception handler)来捕获。
Objective-C的错误模型表明,异常只应在发生严重错误后抛出。虽说如此,有时仍然需要捕获并处理异常,例如使用Objective-C++来编码时,或编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制。此外,有些系统库也会用到异常。
捕获异常时,一定要注意将try块内所创建的对象都清理干净。
在默认情况下,ARC不生成安全处理异常所需的清理代码。开启-fobc-arc-exceptions编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。但最重要的是:在发生大量异常捕获操作时,应考虑重构代码,用NSError式错误信息传递法来取代异常。
有种情况下编译器会自动开启-fobc-arc-exceptions编译器标志,就是处于Objective-C++模式时。因为C++处理异常的代码与ARC实现的附加代码类似,故令ARC加入安全处理异常的代码,其性能损失不会太大。由于C++频繁使用异常,故Objective-C++程序员很可能也会使用异常。
对象图里经常出现一种情况,即几个对象以某种方式相互引用,从而形成“环”。由于Objective-C内存管理模型采用引用计数架构,所以这种情况就会导致内存泄漏。保留环里的对象无法为外界所访问,但这些对象之间尚有引用,使得它们都能继续存活下去,而不会为系统所回收。
避免保留环的最佳方式是弱引用。这种引用常用来表示“非拥有关系”(non-owning relationship)。将属性声明为unsafe-unretained或者weak即可。unsafe_unretained表明属性可能不安全,而且不归此实例所拥有,语义与assign等价,但前者多用于修饰对象类型,后者通常只用于基本类型(integral types),例如int、float、结构体等。weak属性特质与unsafe_unretained作用完全相同,只是当系统将属性回收后,属性值就自动设为nil。使用weak比unsafe_unretained可以令代码更安全。不过无论如何,只要在所指对象已经彻底销毁后还继续使用弱引用,那就依然是个bug。
Objective-C对象的生命期取决于其引用计数。释放对象有两种方式:一种是release
方法,使其保留计数立即递减;另一种是autorelease
方法,将其加入自动释放池中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain
)自动释放池时,系统会向其中的对象发生release
消息。
Mac OS X/iOS系统会自动创建一些线程,比如主线程或GCD机制中的线程,这些线程默认都有自动释放池,每次执行事件循环时就会将其清空。通常只有一个地方需要创建自动释放池,即main函数里,用来包裹应用程序主入口点(main application entry point):
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc,
argv,
nil,
@"EOCAppDelegate");
}
}
自动释放池于{处创建,并于对应的}处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。
可以利用自动释放池来降低应用程序在运行过程中的内存峰值。加入自动释放池块后,系统会在块的末尾把它包裹的对象回收掉。例如:
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
自动释放池机制就像栈(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池则相当于将其从栈中弹出。在对象上执行自动释放池操作,就等于将其放入栈顶的那个池里。
在ARC出现之间,自动释放池有种老式写法,即使用NSAutoreleasePool
对象。然而@autoreleasepool这种写法能创建出更为轻便的自动释放池。
启用Cocoa提供的僵尸对象调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
打开XCode->Product->Edit Scheme…->Run界面,在Arguments选项卡下添加NSZombieEnabled环境变量并将其值设置为YES,或者在Diagnostics选项卡下勾选Enable Zombie Objects,即可启动僵尸对象功能。
僵尸类是从名为_NSZombie_的模板类里复制出来的。这些僵尸类没有多余的功能,只是充当一个标记,即保留了旧类名。下列伪代码演示了系统如何根据需要创建僵尸类,而僵尸类又如何把待回收的对象转化成僵尸对象:
// Obtain the class of the object being deallocated
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class_getName(cls);
// Prepend _NSZombie_ to the class name
const char *zombieClsName = "_NSZombie_" + clsName;
// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);
// If the specific zombie class doesn't exist,
// then it needs to be created
if (!zombieCls) {
// Obtain the template zombie class called _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
// Duplicate the base zombie class, where the new class's
// name is the prepended string from above
zombieCls = objc_duplicateClass(baseZombieCls,
zombieClsName, 0);
}
// Perform normal destruction of the object being deallocated
objc_destructInstance(self);
// Set the class of the object being deallocated
// to the zombie class
objc_setClass(self, zombieCls);
// The class of 'self' is now _NSZombie_OriginalClass
这个过程其实就是NSObject
的dealloc
方法所做的事。运行期系统如果发现启动僵尸对象功能,那么就把dealloc
方法调配(swizzle)成一个会执行上述代码的版本,将对象所属的类变为_NSZombie_OriginClassName,这里OriginClassName指代原类名。
僵尸类的作用体现在消息转发例程。_NSZombie_类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,跟NSObject一样都是根类。此类只有一个实例变量isa,所有Objective-C的根类都必须有此变量。由于此类没有实现任何方法,所有发给它的全部消息都要经过“完整的消息转发机制”(full forwarding mechanism)。
在完整的消息转发机制中,___forwarding___是核心。它首先要做的事情就包括检查接收消息的对象所属类的名称,如果前缀为_NSZombie_,需要特殊处理。此时会打印一条消息,指明僵尸对象所收到的消息和原类名,然后应用程序就终止了。获取原类名的思路很简单:将僵尸类名的前缀_NSZombie_去掉即可。打印消息示例如下:
*** -[CFString respondsToSelector:]: message sent to
deallocated instance 0x7ff9e9c080e0
NSObject协议中定义了下列方法,用于查询对象当前的引用计数:
- (NSUInteger)retainCount
然而在ARC中不能调用此方法,否则会跟在ARC中调用retain、release和autorelease方法时一样编辑器会报错。在MRC(手动引用计数)下仍可使用此方法。
问题在于,保留计数的绝对值一般都与开发者所应留意的事情完全无关。即便在调试时才调用此方法,通常也无所助益。
此方法之所以无用的首要原因在于:它所返回的引用计数值只是某个给定时间点上的值,并未考虑到系统会稍后把自动释放池清空,因而不会将后续的自动释放操作从返回值里减去。如果据此直接将引用计数递减到0,那么后续自动释放池执行清空操作时会导致程序奔溃。
再者,retainCount可能永远不会返回0,因为有时系统会优化对象的释放行为,在retainCount未归零的情况下直接将对象回收。
此外,有些常量或变量的retainCount可能不是正常值,例如常量字符串,单例对象,其保留计数永远不会变。这时的保留和释放操作都是空操作。
那何时才应该使用retainCount呢?最佳答案是绝对不要用,尤其考虑到苹果公司在引入ARC之后已正式将其废弃,就更不应该用了。