第五章|第五章 内存管理—第30条:以ARC简化引用计数

引用计数这个概念相当容易理解(参见第29条)。需要执行保留与释放操作的地方也很容易就能看出来。所以Clang编译器项目带有一个"静态分析器"(static analyzer),用于指明程序里引用计数出问题的地方。举个例子,假设下面这段代码采用手工方式管理引用计数:

if ([self shouldLogMessage]) { NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self]; NSLog(@"message = %@", message); }

此代码有内存泄漏问题,因为if语句块末尾并未释放message对象。由于在if语句之外无法引用message,所以此对象所占的内存泄漏了。判定内存是否泄漏所用的规则很简明:调用NSString的alloc方法所返回的那个message对象的保留计数比期望值要多1.然而却没有与之对应的释放操作来抵消。因为这些规则很容易表述,所以计算机可以简明地将其套用在程序上,从而分析出有内存泄漏问题的对象。这正是"静态分析器"要做的事。
使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加的。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。不过,ARC的那些功能都是基于核心的内存管理语义而构建的,这套标准语义贯穿于整个Objective-C语言。
由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:
- retain - release - autorelease - dealloc

【第五章|第五章 内存管理—第30条:以ARC简化引用计数】直接调用上述任何方法都会产生编译错误,因为ARC要分析何处应该自动调用内存管理方法,所以如果手动调用的话,就会干扰其工作。此时必须信赖ARC,令其帮你正确处理内存管理事宜,而这会使那些手动管理引用计数的开发者不太放心。
实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层的C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objec_retain。这也是不能覆写retain、release或autorelease的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的Objective-C方法来指代与之相关的底层C语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。
使用ARC时必须遵循的方法命名规则
将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
- alloc - new - copy - mutableCopy

归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease,那么保留计数的值可能比1大,这也是retainCount方法不太有用的原因之一(参见第36条)。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
维系这些规则所需的全部内存管理事宜均由ARC自动处理,其中也包括在将要返回的对象上调用autorelease,下列代码演示了ARC的用法:
- (EOCPerson*)newPerson { EOCPerson *person = [[EOCPerson alloc] init]; return person; /** * The method name begins with `new’, and since `person’ * already has an unbalanced +1 reference count from the * `alloc’, no retains, releases or autoreleases are * required when returning. */ }- (EOCPerson*)somePerson { EOCPerson *person = [[EOCPerson alloc] init]; return person; /** * The method name does not begin with one of the "owning" * prefixes, therefore ARC will add an autorelease when * returning `person’. * The equivalent manual reference counting statement is: *return [person autorelease]; */ }- (void)doSomething { EOCPerson *personOne = [self newPerson]; // …EOCPerson *personTwo = [self somePerson]; // …/** * At this point, `personOne’ and `personTwo’ go out of * scope, therefore ARC needs to clean them up as required. * - `personOne’ was returned as owned by this block of code, so it needs to be released. * - `personTwo’ was returned not owned by this block of code, so it does not need to be released. * The equivalent manual reference counting cleanup code * is: *[personOne release]; */ }

ARC通过命名约定将内存管理规则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像Objective-C这样强调命名。但是,想成为优秀的Objective-C程序员就必须适应这套理念。在编码过程中,ARC能帮程序员做许多事情。
除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手动操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够互相抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么ARC有时可以成对地移除这两个操作。
ARC也包含运行期组件。此时所执行的优化很有意义,大家看过之后就会明白为何以后的代码都应该用ARC来写了。前面讲到,某些方法在返回对象前,为其执行了autorelease操作,而调用方法的代码可能需要将返回的对象保留,比如像下面这种情况就是如此:
// From a class where _myPerson is a strong instance variable _myPerson = [EOCPerson personWithName:@"Bob Smith"];

调用"personWithName:"方法会返回新的EOCPerson对象,而此方法在返回对象之前,为其调用了autorelease方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效:
EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"]; _myPerson = [tmp retain];

变量的内存管理语义
ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同。例如,有下面这段代码:
@interface EOCClass : NSObject { id _object; }@implementation EOCClass - (void)setup { _object = [EOCOtherClass new]; } @end

在手动管理引用计数时,实例变量_object并不会自动保留其值,而在ARC环境下则会这样做。也就是说,若在ARC下编译setup方法,则其代码会变为:
- (void)setup { id tmp = [EOCOtherClass new]; _object = [tmp retain]; [tmp release]; }

当然,在此情况下,retain和release可以消去。所以,ARC会将这两个操作化简掉,于是,实际执行的代码还是和原来一样。不过,在编写设置方法(setter)时,使用ARC会简单一些。如果不用ARC,那么需要像下面这样来写:
- (void)setObject:(id)object { [_object release]; _object = [object retain]; }

但是这样写会出问题。假如新值和实例变量已有的值相同,会如何呢?如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。使用ARC之后,就不可能发生这种疏失了。在ARC环境下,与刚才等效的设置函数可以这么写:
- (void)setObject:(id)object { _object = object; }

ARC会用一种安全的方式来设置: 先保留新值,再释放旧值,最后设置实例变量。在手动管理引用计数时,你可能已经明白这个问题了,所以应该能正确编写设置方法,不过用了ARC之后,根本无须考虑这种"边界情况"(edge case)。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
  • __strong: 默认语义,保留此值
  • __unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
  • weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
  • __autoreleasing: 把对象"按引用传递"(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
    比方说,想令实例变量的语义与不使用ARC时相同,可以运用__weak 或__unsafe_unretained修饰符:
@interface EOCClass : NSObject { id __weak _weakObject; id __unsafe_unretained _unsafeUnretainedObject; }

不论采用上面哪种写法,在设置实例变量时都不会保留其值。
我们经常会给局部变量加上修饰符,用以打破由"块"(block, 参见第40条)所引入的"保留环"(retain cycle)。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致"保留环"。可以用__weak局部变量来打破这种"保留环":
NSURL *url = [NSURL URLWithString:@"http://www.example.com/"]; EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url]; EOCNetworkFetcher * __weak weakFetcher = fetcher; [fetcher startWithCompletion:^(BOOL success){ NSLog(@"Finished fetching from %@", weakFetcher.url); }];

ARC如何清理实例变量
刚才说过,ARC也负责对实例变量进行内存管理。要管理好其内存,ARC就必须在"回收分配给对象的内存(deallocate)"时生成必要的清理代码(cleanup code)。凡是具备强引用的变量,都必须释放,ARC会在dealloc方法中插入这些代码。当手动管理引用计数时,你可能会像下面这样自己来编写dealloc方法:
- (void)dealloc { [_foo release]; [_bar release]; [super dealloc]; }

用了ARC之后,就不需要再编写这种dealloc方法了,因为ARC会借用Objective-C++的一项特性来生成清理例程(cleanup routine)。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,在该方法中生成清理内存所需的代码。
要点
  • 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多"样板代码"。
  • ARC管理对象生命期的办法基本上就是:在合适的地方插入"保留"及"释放"操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手动执行"保留"及"释放"操作。
  • 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
  • ARC只负责管理Objective-C对象的内存。尤其要注意: CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

    推荐阅读