前面几节主要从对象在内存中的生命周期这个角度,梳理了一下objc的内存管理特性。接下来说几个和内存管理有密切关系的语言特性。
本节主要看一下objc的异常处理部分。我们都知道objc也提供了try-catch机制,但是在实际开发中却很少使用到,这是为什么呢?
为了方便说明问题,先不考虑ARC,假设在非ARC模式下,有如下代码:
@try {
ClassTest *t = [ClassTest alloc] init];
// 此函数可能会抛出异常
[t doSomethingMayThrowException];
[t release];
}
@catch(NSException *e) {
NSLog(@"Oops, there was an exception");
}
上面的代码会产生什么问题吗?按照try-catch的逻辑,如果在方法doSomethingMayThrowException执行时真的抛出异常,try块的代码执行会终止,程序跳转到catch部分,那么实例t的release方法就不会被执行,从而造成内存泄露。
当然,这个问题也有解决方式,就是补充finally块来释放对象,保证对象一定会被释放,如下:
ClassTest *t;
@try {
t = [ClassTest alloc] init];
// 此函数可能会抛出异常
[t doSomethingMayThrowException];
}
@catch(NSException *e) {
NSLog(@"Oops, there was an exception");
}
@finally {
[t release];
}
为了在finally块里面引用t对象,首先需要将t的定义挪到try块外面,由于本示例中只有一个对象,所以感觉还不是特别麻烦。想象一下在实际的工程中,所有的对象都需要这样实现,是不是感觉还是十分头疼的。而且,随着代码量的增加,很容易就忘记释放某个对象,就会导致内存泄露。若泄露的对象是文件描述符或数据库连接之类的稀缺资源,就可能导致比较大的问题。如果try块里面的代码又有try-catch嵌套,那问题就更麻烦了。
上面说的是非ARC环境,那么在ARC环境下怎么样呢?系统会不会帮我们搞定了一切,我们只需要放心使用就可以了呢?很遗憾,不是。在ARC环境下,问题反而更严重了,因为不能再手动调用release了,所以无法在finally块里面手动实现release,必然会造成内存泄露。
为什么ARC不自动去处理这些内存管理问题?主要是从性能角度去考虑。前面的几个章节介绍过,ARC并非是objc的一个语言特性,而是一个编译器福利,即使打开ARC,objc的内存管理还是通过引用计数去实现,只是编译器在编译时,自动帮用户插入了retain、release等调用代码。那么对于try-catch机制来说,要想自动处理好内存管理,在try块开始之前要保存所有块中的变量,这样做需要插入大量的样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。为了达到这个目的,付出的代价就是运行期性能的降低,以及添加进来的大量的额外代码会明显增加应用程序的大小。因此,默认情况下ARC是不会对try-catch机制有特殊处理的。关于try-catch的实现比较复杂,可以参考源代码https://opensource.apple.com/source/objc4的objc-exception.mm等实现文件帮助理解。
虽然默认情况下ARC不会自动处理try-ctach代码块,但苹果也为用户提供了这种能力:就是-fobjc-arc-exceptions选项。打开这个编译器标志可以在ARC模式下较好的处理try-catch的内存管理问题,代价就是性能的下降以及程序体积的上升,而且这也并不意味着所有的问题都会被正确的处理。
也是基于以上这些原因,在objc的中比较少使用到try-catch机制。通常只有当应用程序必须因异常状况而终止时才使用,这时由于应用程序即将终止,即使发生内存泄露也无关紧要了。而在其他场景下,可以使用NSError机制来处理程序错误。