第30条:以ARC简化引用计数
本条要点:(作者总结)
- 有 ARC 之后,程序员就无须担心内存管理问题了。使用 ARC 来编程,可省去类中的许多 “样板代码”。
- ARC 管理对象生命期的办法基本上就是:在合适的地方插入 “保留” 及 “释放”操作。
- 在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来需要手工执行 “保留” 及 “释放”操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。
- ARC 只负责管理 Objective-C 对象的内存。尤其要注意: CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease。
引用计数这个概念相当容易理解。需要执行保留与释放操作的地方也很容易就能看出来。所以 Clang 编译器项目带有一个 “静态分析器”(static analyzer)。用于指明程序里引用计数出问题的地方。举个例子,假设下面这段代码采用手工方式管理引用计数:
1 if ([self shouldLogMessage]) { 2 3 NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self]; 4 5 NSLog(@"Message = %@", message); 6 7 }
此代码有内存泄漏问题,因为 if 语句块末尾并未释放 message 对象。由于在 if 语句之外无法引用 message,所以此对象所占的内存泄漏了(这里“泄漏”的意思是:没有正确释放已经不再使用的内存)。判定内存是否泄漏所用的规则很简明:调用NSString 的 alloc 方法所返回的那个 message 对象的保留计数比期望值要多 1。然而却没有与之对应的释放操作来抵消。因为这些规则很容易表述,所以计算机可以简单地将其套用在程序上,从而分析出有内存泄漏问题的对象。这正是 “静态分析器” 要做的事。
静态分析器还有更为深入的用途。既然可以查明内存管理问题,那么应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题,对吧?自动引用计数这一思路正是源于此。自动引用计数所做的事情与其名称相符,就是自动管理引用计数。于是,在前面那段代码的 if 语句块结束之前,可以于 message 对象上自动执行 release 操作,也就是把代码自动改写为下列形式:
1 if ([self shouldLogMessage]) { 2 3 NSString *message = [[NSString alloc] initWithFormat:@"I am object , %p", self]; 4 NSLog(@"message = %@", message); 5 [message release]; ///< Added by ARC 6 }
使用 ARC 时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由 ARC 自动为你添加。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义之外,ARC 还有更多的功能。不过,ARC 的那些功能都是基于核心的内存管理语义而构建的,这套标准语义贯穿于整个 Objective-C 语言。
由于 ARC 会自动执行 retain、release 、autorelease 等操作,所以直接在 ARC 下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:
- retain
- release
- autorelease
- dealloc
直接调用上述任何方法都会产生编译错误,因为 ARC 要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。此时必须信赖 ARC,令其帮你正确处理内存管理事宜,而这会使那些惯于手动管理引用计数的开发者不太放心。
实际上,ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多 CPU 周期。比方说,ARC 会调用与 retain 等价的底层函数 objc_retain。这也是不能覆写 retain、release 或 autorelease 的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的 Objective-C 方法来指代与之相关的底层 C 语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。
使用 ARC 时必须遵循的方法命名规则
将内存管理语义在方法名中表示出来早已成为 Objective-C 的惯例,而 ARC 则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
- alloc
- new
- copy
- mutableCopy
归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了 autorelease,那么保留计数的值可能比 1 大,这也是 retainCount 方法不太有用的原因之一。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。 在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
维系这些规则所需的全部内存管理事宜均由 ARC 自动处理,其中也包括在将要返回的对象上调用 autorelease,下列代码演示了 ARC 的用法:
1 + (EOCPerson *)newPerson { 2 3 EOCPerson *person = [[EOCPerson alloc] init]; 4 5 return person; 6 /** 7 8 * The method name begins with 'new', and since 'person' 9 * already has an unbalanced + 1 retain count from the 10 11 * 'alloc', no retain, release, or autorelease are 12 13 * required when returning. 14 15 */ 16 17 }
1 + (EOCPerson *)somePerson { 2 3 EOCPerson *person = [[EOCPerson alloc] init]; 4 5 return person; 6 7 /** 8 9 * The method name does not begin with one of the "owning" 10 11 * prefixes, therefore ARC will add an autorelease when 12 13 * returning 'Person' 14 15 * The equivalent manual reference counting statement is: 16 17 * return [person autorelease]; 18 19 */ 20 21 }
1 - (void)doSomething { 2 3 EOCPerson *personOne = [EOCPerson newPerson]; 4 5 // ... 6 7 EOCPerson *personTwo = [EOCPerson somePerson]; 8 9 // ... 10 11 /** 12 13 * At the point, 'personOne' and 'personTwo' go out of scope, therefore ARC needs to clean them up as required. - 'personOne' was returned as owned by this blok of code, so it needs to be released. 14 15 * - 'personTwo' was returned not owned by this block of code , so it does not needs to be released. 16 17 * The equivalent manual reference counting cleanting cleanup code is: [PersonOne release]; 18 19 */ 20 21 }
ARC 通过命名约定将内置管理准则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像 Objective-C 这样强调命名。但是,想成为优秀的 Objective-C 程序员就必须适应这套理念。在编码过程中,ARC 能帮助程序员做许多事情。
除了会自动调用 “保留” 与 “释放” 方法外,使用 ARC 还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化,例如,在编译器,ARC 会把能够互相抵消的retain、release、autorelease 操作约简。如果发现在同一对象上执行了多次 “保留” 与 “释放” 操作,那么 ARC 有时可以成对地移除这两个操作。
ARC 也包含运行期组件。此时所执行的优化很有意义,大家看到之后就会明白为何以后的代码都应该用 ARC 来写了。前面讲到,某些方法在返回对象前,为其执行了 autorelease 操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此:
1 // From a class where _myPerson is a strong instance variable 2 3 _myPerson = [EOCPerson personWithName:@"Bob smith"];
调用 “personWithName:” 方法会返回新的 EOCPerson 对象,而此方法在返回对象之前,为其调用了 autorelease 方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效:
1 EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"]; 2 3 _myPerson = [tmp retain];
此时应该能看出来, “personWithName:” 方法里面的 autorelease 与上段代码中的 retain 都是多余的。为提升性能,可将二者删去。但是,在 ARC 环境下编译代码时,必须考虑 “向后兼容性”(backward compatibility),以兼容那些不使用 ARC 的代码。其实本来 ARC 也可以直接舍弃 autorelease 这个概念,并且规定,所有从方法中返回的对象其保留计数都比期望值多 1。但是,这样做就破坏了向后兼容性。
不过,ARC 可以在运行期检测到这一对多余的操作,也就是 autorelease 及紧跟其后的 retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的 autorelease 方法,而是改为调用 objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码要在返回的对象上执行 retain 操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位,而不执行 autorelease 操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行 objc_retainAutoreleasedReturnValue 函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行 retain 操作。设置并检测标志位,要比调用 autorelease 和 retain 更快。
下面这段代码演示了 ARC 是如何通过这些特殊函数来优化程序的:
1 // Within EOCPerson class 2 3 + (EOCPerson *)personWithName:(NSString *)name { 4 5 EOCPerson *person = [[EOCPerson alloc] init]; 6 7 person.name = name; 8 9 objc_autoreleaseReturnValue(person); 10 11 }
1 // Code using EOCPerson class 2 3 EOCPerson *tmp = [EOCPerson personWithName:@"Matt Galloway"]; 4 5 _myPerson = objc_retainAutoreleasedReturnValue(tmp);
为了求得最佳效率,这些特殊函数的实现代码都因处理器而异。下面这段伪代码描述了其中的步骤:
1 id objc_autoreleaseReturnValue(id object) { 2 3 if (/* caller will retain object*/) { 4 5 set_flag(object); 6 return object; ///< No autorelease 7 } else { 8 9 return [object autorelease]; 10 11 } 12 13 }
1 id objc_retainAutoreleasedReturnValue (id object) { 2 3 if (get_flag(object)) { 4 clear_flag(object); 5 return object; ///< No retain 6 } else { 7 return [object retain]; 8 } 9 10 }
objc_autoreleaseReturnValue 函数究竟如何检测方法调用者是否会立刻保留对象呢?这要根据处理器来定。由于必须查看原始的机器码指令方可判断出这一点,所以,只有编译器的作者才能实现此函数。要想判断出方法调用者会不会保留方法所返回的对象,先得把调用方法的那段代码编排好才行,而这项任务只能由编译器的开发者来完成。
将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化,上面所讲的只是其中一种。我们由此应该可以了解到 ARC 所带来的好处。待编译器与运行期组件日臻成熟,笔者相信还会出现其他优化技术。
变量的内存管理语义
ARC 也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同。例如,有下面这段代码:
1 @interface EOCClass : NSObject { 2 3 id _object; 4 } 5 6 @implementation EOCClass 7 8 - (void)setup { 9 10 _object = [EOCOtherClass new]; 11 } 12 13 @end
在手动管理引用计数时,实例变量 _object 并不会自动保留其值,而在 ARC 环境下则会这样做,也就是说,若在 ARC 下编译 setup 方法,则其代码会变为:
1 - (void)setup { 2 3 id tmp = [EOCOtherClass new]; 4 5 _object = [tmp retain]; 6 7 [tmp release]; 8 9 }
当然,在此情况下,retain 和 release 可以消去。所以,ARC 会将这两个操作化简掉,于是,实际执行的代码还是和原来一样。不过,在编写设置方法(setter)时,使用 ARC 会简单一些。如果不用 ARC ,那么需要像下面这样来写:
1 - (void)setObject:(id)object { 2 3 [_object release]; 4 5 _object = [object retain]; 6 7 }
但是这样写会出问题。假如新值和实例变量已有的值相同,会如何呢?如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。使用 ARC 之后,就不可能发生这种疏失了。在 ARC 环境下,与刚才等效的设置函数可以这么写:
1 - (void)setObject:(id)object { 2 3 _object = object; 4 5 }
ARC 会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。在手动管理引用计数时,你可能已经明白这个问题了,所以应该能正确编写设置方法,不过用了 ARC 之后,根本无须考虑这种 “边界情况”(edge case)。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
- __strong: 默认语义,保留此值。
- __unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
- __weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
- __autoreleasing: 把对象 “按引用传递” (pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
比方说,想令实例变量的语义与不使用 ARC 时相同,可以运用 __weak 或 __unsafe_unretained 修饰符:
1 @interface EOCClass : NSObject { 2 3 id _weak _weakObject; 4 5 id _unsafe_unretained _unsafeUnretainedObject; 6 7 }
不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版 (Mac OS X 10.7、iOS 5.0 及其后续版本) 运行期程序库时,加了 __weak 修饰符的 weak 引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。
我们经常会给局部变量加上修饰符,用以打破由“块”(block),所引入的“保留环”(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致 “保留环”。可以用 __weak 局部变量来打破这种 “保留环”:
1 NSURL *url = [NSURL URLWithString:@"http://www.example.com/"]; 2 3 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url]; 4 5 EOCNetworkFetcher *__weak weakFetcher = fetcher; 6 7 [fetcher startWithCompletion:^(BOOL success) { 8 NSLog(@"Finished fetching from %@", weakFetcher.url); 9 }];
ARC 如何清理实例变量
刚才说过,ARC 也负责对实例变量进行内存管理。要管理其内存,ARC 就必须在 “回收分配给对象的内存”(deallocate)("deallocate" 也称为 “释放(内存)”,然而在 Objective-C 中,“释放” 一词与 “release”操作相对应,所以为了避免混淆,译文改用“回收”、“解除分配” 等说法来对应 “dealoocate”) 时生成必要的清理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC 会在 dealloc 方法中插入这些代码。当手动管理引用计数时,你可能会像下面这样自己来编写 dealloc 方法:
1 - (void)dealloc { 2 3 [_foo release]; 4 5 [_bar release]; 6 7 [super dealloc]; 8 9 }
用了 ARC 之后,就不需要再编写这种 dealloc 方法了,因为 ARC 会借用 Objective-C++ 的一项特性来生成清理例程(cleanup routine)。回收 Objective-C++ 对象时,待回收的对象会调用所有 C++ 对象的析构函数(destructor)。编译器如果发现某个对象里含有 C++ 对象,就会生成名为 .cxx_destruct 的方法。而 ARC 则借助此特性,在该方法中生成清理内存所需要的代码。
不过,如果有非 Objective-C 的对象,比如 CoreFoundation 中的对象或是由 malloc() 分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的 dealloc 方法。前文说过,在 ARC 下不能直接调用 dealloc。ARC 会自动在 .cxx_destruct 方法中生成代码并运行此方法,而在生成的代码中会自动调用超类的 dealloc 方法。ARC 环境下,dealloc 方法可以像这样写:
1 - (void)dealloc { 2 3 CFRelease(_coreFoundationObject); 4 5 free(_heapAllocatedMemoryBlob); 6 7 }
因为 ARC 会自动生成回收对象时所执行的代码,所以通常无须再编写 dealloc 方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码(boilerplate code)。
覆写内存管理方法
不使用 ARC 时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写 release 方法,将其替换为 “空操作”(no-op)。但在 ARC 环境下不能这么做,因为会干扰到 ARC 分析对象生命期的工作。而且,由于开发者不可调用及覆写这些方法,所以 ARC 能够优化 retain、release、autorelease 操作,使之不经过 Objective-C 的消息派发机制。优化后的操作,直接调用隐藏在运行期程序中的 C 函数。这就意味着 ARC 可以执行各种优化了,比如刚才提到:如果方法命令即将返回的对象稍后 “自动释放”,而方法调用者立刻“保留”这个返回后对象,那么这两个操作就会为 ARC 所化简。
END