ARC(Automatic Reference Counting,自动引用计数)极大地减少了Cocoa开发中的常见编程错误:retain
跟release
不匹配。ARC并不会消除对retain
和release
的调用,而是把这项原本大都属于开发者的工作移交给了编译器。这样做的好处是显而易见的,但是必须知道retain
和release
是仍然在使用的。ARC并不等同垃圾回收。思考下面这段代码,它对一个实例变量赋值:
1 2 3 |
@property (nonatomic, readwrite, strong) NSString *title;
...
_title = [NSString stringWithFormat:@"Title"];
|
在非ARC环境中,_title
没有被保留过,那么赋给它的NSString
就是autorelease
的,会在事件循环结束时被释放。之后如果再次访问_title
,程序就会崩溃。这类错误极其常见,调试起来会异常困难。另外,如果_title
之前已经有一个值,那旧的值就会因为没有被释放掉而引起内存泄漏。
使用ARC,编译器会在合适的位置自动插入一些代码,下面的代码与前面的代码是等同的:
1 2 3 4 |
id oldTitle = _title;
_title = [NSString stringWithFormat:@"Title"];
[_title retain];
[oldTitle release];
|
retain
和release
仍然会被调用,所以有一些开销,在release
的时候可能还会调用dealloc
方法。这段代码与程序员手动调用retain
和release
的代码在运行结果上是完全一致的。垃圾回收机制是在运行时起作用的,会影响运行效率,而ARC是在编译时插入内存管理代码,不影响运行时效率,因此内存回收比垃圾回收时的效率要高,能够提升系统性能。正如其他一些编译器优化方式一样,这种编译器可以自由地以多种方式优化内存管理,而让程序员手动去做这些工作是不现实的。在多数情况下,使用ARC生成内存管理代码的程序比程序员手工添加内存管理代码的对等程序运行更快!
ARC不是垃圾回收,尤其是它不能像Snow Leopard中的垃圾回收机制那样处理循环引用(保留)。图3-1中的对象A与对象B之间存在循环保留。
图3-1 循环保留
在Snow Leopard的垃圾回收机制下,如果从“外部对象”到“对象A”的引用链接中断了,对象A和对象B都会被销毁,因为它们从程序中孤立出去了。而在ARC中,对象A和对象B都不会被销毁,因为它们的引用计数都大于0。因此,在iOS开发中,必须要做好对强引用(strong reference)的跟踪管理以免出现循环引用。
属性关系有两种主要类型:strong
和weak
,相当于非ARC环境里的retain
和assign
。只要存在一个强引用,对象就会一直存在,不会被销毁。强引用类似于C++中的shared_ptr
,只不过管理引用计数的代码是在编译时生成的,而shared_ptr
是在运行时通过操作符重载确定的。
Objective-C中一直存在循环引用的问题,但在实际应用中很少出现循环引用。对于过去那些使用assign
属性的地方,在ARC环境中要使用weak
代替。大部分引用循环是由委托(delegate)引起的,所以应该总是把delegate
属性声明为weak
。当引用的对象被销毁之后,weak
引用会被自动置为nil
,与assign
相比这是一个巨大的进步,因为assign
可以指向被释放掉的内存,导致程序崩溃。
在ARC出现以前,合成属性(synthesized property)的默认存储类型是assign
;而在ARC中,默认的存储类型是strong
。这在进行代码转换时可能会造成一些困惑。建议为所有属性显式指明存储类型。 循环保留产生的另一个主要原因是块,其介绍详见第23章。
将代码转换到ARC的时候,要注意以下两点。
- 不要再使用
retain
、release
、autorelease
,可以直接将这些代码删除。ARC在编译时会自动在合适的位置插入内存管理代码。 - 如果
dealloc
方法只是用于释放实例变量,那么dealloc
方法也没有存在的必要了,可以直接删掉。在任何情况下都不需要再使用release
,ARC会自动处理好这些事情。如果仍然需要使用dealloc
来做一些其他的事情(比如移除KVO观察),记得不要再调用[super dealloc]
,否则编译器会给出错误信息。
正如前面所说的,ARC并不是垃圾回收,它是编译器的一种功能,可以在代码中合适的位置自动插入调用retain
和release
的语句。这也意味着,如果现存的使用手动内存管理方式的代码很好地遵守了命名约定,那么这些代码跟ARC就是完全互通的。举例来说,如果调用了一个名为copySomething
的方法,ARC会认为这个方法会对返回对象的引用计数加1,如果有必要,ARC会在合适的位置插入一个release
。引用计数的加1操作既可以在ARC代码中进行,也可以在某段手动进行内存管理的代码中进行,这对于ARC来说是无所谓的。
但是,如果没有遵守Cocoa命名约定,这种情况下就会出错。比如有一个名为copyRight
的方法,它以autorelease
的形式返回一个版权信息的字符串,这种情况下ARC的行为取决于调用代码和被调用代码是否都是使用ARC进行编译的。
方法名是以copy
开头的,因此ARC认为copyRight
方法会对返回对象的引用计数加1。如果copyRight
方法的代码与调用copyRight
方法的代码都是使用ARC编译的,ARC就会在copyRight
方法代码中添加一个retain
语句,同时在调用copyRight
方法的代码中添加一个release
语句,这种情况下程序依然会正常工作。虽然对执行效率有一点点影响,但是程序不会崩溃,也不会造成内存泄漏。
然而,如果调用copyRight
方法的代码是使用ARC编译的(ARC就会在代码后边添加一个release
语句),而copyRight
方法代码没有使用ARC编译的话(方法体中就没有retain
语句),程序就会崩溃。反过来,如果copyRight
方法代码使用ARC编译(ARC就会在方法代码中添加一个retain
语句),而调用代码没有使用ARC编译的话(调用代码中就不会有release
语句),代码就会造成内存泄漏。
最好的解决办法就是遵守Cocoa的命名约定。在这个例子中,如果将方法名改为copyright
,就可以完全避免这种问题。ARC根据驼峰式大小写方式对方法的名称进行单词分隔,然后使用相应的内存管理规则。
如果对一个错误命名的方法进行重命名是不现实的,那么还可以在方法声明中使用NS_RETURNS_RETAINED
或者NS_RETURNS_NOT_RETAINED
修饰符来告诉编译器应该使用哪种内存管理规则。这些修饰符是在NSObjCRuntime.h中定义的。
为了让编译器能够恰当地添加retain
和release
语句,编写ARC代码时需要注意以下四点。
-
不要调用
retain
、release
和autorelease
。直接将这些代码删除,这是最简单的规则。另外,也不可以覆盖这几个方法(永远不要试图去覆盖这几个方法)。如果你想实现Singleton模式,请阅读第4章,了解如何在不覆盖这些方法的情况下实现Singleton模式。 -
C语言结构体中不要有对象指针。这种情况很少出现。但是如果C语言结构体中确实有一个对象,要么把它存储到另一个对象中,要么就把它转换成
void*
类型(下一条规则会介绍如何转换到void*
)。可以在任何时候通过调用free
方法销毁一个C语言结构体,这会影响到对结构体中对象的自动跟踪。 -
id
和void*
类型只能通过桥接转换进行转换。在使用了Core Foundation的代码中会有很多这种情况。第27章会详细介绍桥接转换及如何结合ARC来使用。 -
不要使用
NSAutoreleasePool
。不要手动创建自己的自动释放池,直接把需要自己的池的代码放入@autoreleasepool{}
代码块中即可。如果你有一些特殊的代码用于控制自动释放池的释放,那些代码实际上很可能是不必要的。@autoreleasepool
比NSAutoreleasePool
快了大约20倍。
对于大部分代码来说,只要遵守这些规则就不会有任何问题。在Xcode中有一个小工具可以替你完成大部分代码转换的工作,它位于Edit → Refactor → Convert to Objective-C ARC。
ARC应该是Objective-C中继自动释放池之后最重要的改进了,应该尽可能地使用。如果不能将代码全部转换到ARC,就尽可能地转换。使用ARC编译的程序速度更快,bug更少,也比手动内存管理代码更容易编写。现在就开始使用ARC吧!