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

2017-05-09  本文已影响24人  CoderCurtis

引用计数这个概念相当容易理解(参见第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

直接调用上述任何方法都会产生编译错误,因为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)。
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

@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则借助此特性,在该方法中生成清理内存所需的代码。

要点

上一篇下一篇

猜你喜欢

热点阅读