第32条:编写“异常安全代码”时留意内存管理问题
本条要点:(作者总结)
- 捕获异常时,一定要注意将 try 块所创立的对象清理干净。
- 在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
许多时下流行的编程语言都提供了 “异常”(exception)这一特性。纯 C 中没有异常,而 C++ 与 Objective-C 都支持异常。实际上,在当前的运行期系统中,C++ 与 Objective-C 的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序”(exception handler)来捕获。
Objective-C 的错误模型表明,异常只应在发生严重错误后抛出,虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如使用 Objective-C++ 来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,这使我们想起从前那个频繁使用异常的年代。比如,在使用 “键值观测”(KVO)功能时,若想注销一个尚未注册的“观察者”,便会抛出异常。
发生异常时应该如何管理内存是个值得研究的问题。在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch 块能处理此问题,否则对象所占内存就将泄漏。C++ 的析构函数(destructor)由 Objective-C 的异常处理例程(exception-handle routine)来运行。这对于 C++ 对象很重要,由于抛出异常会缩短其生命周期,所以发生异常时必须析构,不然就会泄漏,而文件句柄(file handle)等系统资源因为没有正确清理,所以就更容易因此而泄漏了。
异常处理例程将自动销毁对象,然而在手动管理引用计数时,销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C 代码为例:
1 @try { 2 3 EOCSomeClass *object = [[EOCSomeClass alloc] init]; 4 5 [Object doSomethingThatMayThrow]; 6 7 [Object release]; 8 9 } 10 11 @catch (...) { 12 13 NSLog(@"Whoops, there was an error. Oh well..."); 14 15 }
咋一看似乎没问题,但如果 doSomethingThatMayThrow 抛出异常了呢?由于异常会令执行过程终止并跳至catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决方法是使用 @finally 块,无论是否抛出异常,其中代码都保证会运行,且只运行一次。比方说,刚才那段代码可改写如下:
1 EOCSomeClass *object; 2 3 @try { 4 5 object = [[EOCSomeClass alloc] init]; 6 7 [object doSomethingThatMayThrow]; 8 9 } 10 11 @catch (...) { 12 13 NSLog(@"Whoops, there was an error. Oh well..."); 14 } 15 16 @finally { 17 18 [object release]; 19 }
注意,由于 @finally 块也要引用 object 对象,所以必须把它从 @try 块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且,假如 @try 块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄漏。若泄漏的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这将导致应用程序把所有系统资源都抓在自己手里而不及时释放。
在 ARC 环境下,问题会更严重。下面这段使用 ARC 的代码与修改前的那段代码等效:
1 @try { 2 3 EOCSomeClass *object = [[EOCSomeClass alloc] init]; 4 [object doSomethingThatMayThrow]; 5 } 6 7 @catch (...) { 8 9 NSLog(@"Whoope, there was an error. Oh well..."); 10 }
现在问题更大了:由于不能调用 release,所以无法像手动管理引用计数那样把释放操作移到 @finally 块中。你可能认为这种状况 ARC 自然会处理的。但实际上 ARC 不会自动处理,因为这样做需要加入大量的样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛出异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。
虽说默认情况下未开启,但 ARC 依然能生成这种安全处理异常所用的附加代码。-fobjc-arc-exceptions 这个编译器标志用来开启此功能。其默认不开启的原因是: 在 Objective-C 代码中,只有当应用程序必须因异常状况而终止时才应抛出异常。因此,如果应用程序即将终止,那么是否还会发生内存泄漏就已经无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。
有种情况编译器会自动把 -forjc-arc-exceptions 标志打开,就是处于 Objective-C++ 模式时。因为C++ 处理异常所用的代码与 ARC 实现的附加代码类似,所以令 ARC 加入自己的代码以安全处理异常,其性能损失并不太大。此外,由于 C++ 频繁使用异常,所以 Objective-C++ 程序员很可能也会使用异常。
如果手工管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净。若使用 ARC 且必须捕获异常,则需打开编译器的 -fojc-arc-exceptions 标志。但最重要的是:在发现大量异常捕获操作时,应考虑重构代码,用第21 条所讲的 NSError 式错误信息传递来取代异常。
END