第五章 内存管理—第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

直接调用上述任何方法都会产生编译错误,因为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。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容