Effective Objective-C 2.0 第五章 内存管理 Item 30 用ARC简化引用计数

Item 30 用ARC简化引用计数

引用计数的概念是相当容易理解的。何处需要 retain 和 release 也很容易被表达。于是编译器Clang有一个静态分析器,可用于指出何处的引用计数出了问题。例如如下的手动引用计数代码片段

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

这段代码存在一个内存泄漏,因为 message 对象在 if 语句中未被释放,而它在 if 语句之外不能被持有,所以该对象泄漏了。判断是否泄漏的规则是直截了当的。 NSString 的 alloc 方法返回一个引用计数为 a+1 的对象,但没有对应的 release。这个规则很清楚,计算机也能简单的应用这个规则告诉我们对象被泄漏。这就是静态分析器所做的事。
这个静态分析器做得更多。由于它能够告诉你哪里有内存管理问题,它也能通过增加必要的retain或release,来提前一步解决内存问题。这就是自动引用计数(ARC)的想法的来源。ARC 如同名字所描述的,是的引用计数自动化。在下面的代码片段中,message 对象会在 if 的末尾被自动添加 release 方法:

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

ARC 机制下依然要牢记引用计数仍然在起作用。只不过 ARC 帮你添加了 retain 和 release。你还会看到,ARC 做的不仅仅是在方法返回对象这里。但正是这些核心语义,成为了 ARC 模式下 OC 的标准。
因为 ARC 为你添加了 retain、release、autorelease,在 ARC 模式下直接调用这些内存方法是非法的。以下方法都不能被直接调用:

  • retain
  • release
  • autorelease
  • dealloc

直接调用这些方法将导致编译错误因为这样会对 ARC 的内存管理造成干扰。你必须相信 ARC,对于惯用了手动引用计数的开发者来说,这一点有点令人生畏。

事实上,ARC不使用一般Objective-C的消息派送机制,而是调用了更底层的C函数,以优化性能。因此,retain、release、autorelease方法从不直接调用,也不允许重载。
事实上,ARC 不通过普通的 OC 方法对这些方法进行调用而是通过底层的 C 变体。这是由于 retain 和 release 经常被调用所做的优化,以节约 CPU 的周期。例如 objc_retain 对应 retain。所以说覆写 retain、release、autorelease 是非法的,因为这些方法并未直接被调用。这个 item 剩下的部分,我将更多的讨论 OC 的方法而不是底层的 C 变体。这对习惯了手动引用计数的开发者是个帮助。

ARC 下的方法命名规则
通过方法名来决定内存管理在 OC 中早已成为惯例,ARC 进一步巩固了这个规则。如果一个方法以如下单词开头,那么该方法返回的对象由调用者所持有:

  • alloc
  • new
  • copy
  • mutableCopy

由调用者所持有意味着调用以上四个方法之一的代码要负责释放返回的对象。也就是说,调用代码对对象有着一个大于 0 的引用计数(等于1)需要平衡。如果对象被额外的 retain 和 autorelease,那么引用计数大于 0,所以说方法 retainCount 没什么用。
任何其他的方法名表示返回的对象不被调用者所持有。在这些例子中,返回的对象会被自动释放,只在方法的边界存活。如果想要确保对象存活得更久,调用的代码需要对它进行 retain。
ARC 根据这套规则自动进行内存管理,包括返回自动释放对象的代码,如下所示:

+(EOCPerson *)newPerson {
    EOCPerson *person = [[EOCPerson alloc] init];
    return person;
    /**
    * 方法名以“new”开头, 而“alloc” 返回的 person 已经引用计数 +1
    * 返回时不需要 retains, release, 或 autorelease
    * /
}
+(EOCPerson *)somePerson {
    EOCPerson *person = [[EOCPerson alloc] init];
    return person;
    /**
    * 方法名不以“持有类型”的前缀命名,因此 ARC 将在返回 person 时添加 autorelease
    * 对等的手动引用计数方法应写为:
    * return [person autorelease];
    */
}
+(void)doSomething {
    EOCPerson *personOne = [EOCPerson newPerson];
    EOCPerson *personTwo = [EOCPerson somePerson];
    /**
    * personOne 和 personTwo 已经超出了范围,因此 ARC 需要对他们进行必要的清理
    * personOne 被这段代码所持有,需要被释放
    * personTwo 不被这段代码所持有,所以不需要被释放
    * 对等的手动引用计数清理代码为:
    * [personOne release];
    */
}

ARC 通过命名传统对内存管理规则进行标准化,在这门语言的入门者看来这是很不寻常的。几乎没有一门语言像 OC 一样对命名如此强调。熟悉这一概念对成为一名优秀的 OC 开发者而言是至关重要的。ARC 在这个过程中为我们做了非常多的工作。
除了增加 retain 和 release 之外,ARC 还有其他的作用。它还能完成我们非常困难或不可能完成的优化。例如在编译时 ARC 能移除成对的、多余的retain、release、autorelease操作。譬如如下情形:
ARC 也包含运行时组件。这一优化是更重要的,证明了为什么良好的代码要在 ARC 模式下运行。之前说过,有些对象在返回时被放入自动释放池,有时,调用的代码需要持有返回的对象,如下:

// _myPerson 是某类中的一个被持有的实例变量
_myPerson = [EOCPerson personWithName:@"Bob Smith"];

方法 “-personWithName:" 返回了一个新的 EOCPerson 自动释放对象,但编译器在将它赋予实例变量时,需要增加它的引用计数,因为该实例变量是强引用的。因此,在手动引用计数世界里,如下代码是对等效果的:

EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = [tmp retain];

“personWithName:” 方法中的 autorelease 和 retain 是无关的。将它们一起移除会有益于性能。但是 ARC 编译下的代码需要兼容非 ARC 下的代码。ARC 能够移除 autorelease 的概念并且表示所有方法返回的对象都拥有 a+1 的引用计数值,但这破坏了兼容性。
但 ARC 其实在运行时能够检测这种紧随 autorelease 而来的 retain 的情况,通过一种特殊的函数,当一个对象被 autorelease 时,通过调用函数 objc_autoreleaseReturnValue 来代替直接调用 autorelease 方法。这个函数检测到这段代码在对象从现在的这个方法返回后被立即执行,如果返回的对象会被 retain,则在全局数据结构中设置一个标志位来替代执行 autorelease。同样的,执行 retain 操作的代码会检查这个标志位,如果被设置了,就也用 obje_retainAutoreleaseReturnValue 代替直接调用 retain。这额外的设置和检查标志位的性能比执行 autorelease 和 retain 更快。
以下代码表现了 ARC 使用这些特殊函数对性能的优化:

//Within EOCPerson class
+ (EOCPerson *) personWithName:(NSString *)name {
    EOCPerson *person = [[EOCPerson alloc] init];
    person.name = name;
    objc_autoreleaseReturnValue(person);
}
//Code using EOCPerson class
EOCPerson *tmp = [EOCPerson personWithName:@"Matt Galloway"];
_myPerson = objc_retainAutoreleasedReturnValue(tmp);

这些特殊的函数为达到最优的性能,会有特定于处理器的实现。下面的伪代码解释了所发生的事情:

id objc_autoreleaseReturnValue(id object) {
    if(/* caller will retain object*/) {
        set_flag(object);
        return object;//<NO autorelease
    } else {
        return [object autorelease];
    }
}
id objc_retainAutoreleaseReturnValue(id object) {
    if(gt_flag(object)) {
        clear_flag(object);
        return object;//<NO retain
    } else {
        return [object retain];
    }
}

objc_autoreleaseReturnValue 检测调用代码是否会立即保留对象的方法是特定于处理器的。只有编译器的作者能执行这一点,因为这需要检查原始机器代码指令。编译器的作者是唯一一个能确保代码会按照这样一种方式排列使得检测可以进行的人。
这还只是将内存管理降到编译器和运行时手中的其中一个好处。它能够帮助解释为什么使用 ARC 是一个好主意。当编译器和运行时更加成熟,我相信会有更多优化成为现实。

变量的内存管理语义

ARC 也对局部变量和实例变量进行内存管理。默认的,每一个变量都被对象强持有。理解这一点很重要,尤其是实例变量,因为在特定的代码下,手动引用计数的语义可能会不一样。例如如下代码:

@interface EOCClass : NSObject {
    id _object;
}
@implementation EOCClass
- (void)setup {
    _object = [EOCOtherClass new];
}
@end

实例变量 _object 在手动引用计数下不会被自动持有,而在 ARC 下会被持有。当 setup 方法在 ARC 下被编译时,方法转成如下:

- (void)setup {
    id temp = [EOCOtherClass new];
    _object = [tmp retain];
    [tmp release];
}

当然,此处的 retain 和 release 可以被忽略掉,所以 ARC 下的代码还是跟之前一样。但这在写 setter 时派上了用场。在 ARC 之前,setter 要这么写:

- (void)setObject:(id)object {
    [_object release];
    _object = [object retain];
}

这揭示了一个问题,如果被设置的新值恰好是原本该实例变量持有的旧值,如果这个对象只有这一个持有关系, release 会使得引用计数降为 0,对象可能被销毁。接下来的 retain 将使得程序崩溃。ARC 避免了这种错误的发生。ARC 下的 setter 方法如下:

- (void)setObject:(id)object {
    _object = object;
}

ARC 通过持有新值,然后在设置变量值之前释放旧值,给实例变量提供了一个安全的设置方法。你可能已经在手动计数的环境下理解了这一切,并且能够正确的写出 setter 代码。但是使用 ARC, 你将不再需要担心这些极端情况。
局部变量和实例变量的语义可以在应用中通过以下限定词进行改变:

  • __strong:默认值;被 retain
  • __unsafe_unretained:非持有关系可能是不安全的,重新使用时可能对象已被销毁
  • __weak:不被持有但是安全,对象销毁后指针自动置 nil
  • __autoreleasing:该限定词用于当对象被方法持有时使用,值在返回时 autorelease

例如,为了使得一个实例变量在脱离 ARC 的模式下如常工作,你可以使用 __weak 或者 __unsafe_unretained 属性:

@interface EOCClass : NSObject {
    id __weak _weakObject;
    id __unsafe_unretained _unsafeUnretainedObject;
}

在这两种情况下,这种实例变量时,对象都不会被持有。__weak 限定符自动对弱引用置 nil 只在Mac OS X 10.7 和 iOS 5.0之后的版本有效。
这些限定词常用于打破局部变量的循环引用。block 自动持有所有它捕获的对象,有时会导致循环引用,如果 block 持有的对象也持有 block。一个 __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 在销毁过程中自动生成所需要的清理代码。ARC 把所有需要被释放的强引用的变量套进了 dealloc 方法中。在手动引用计数时,你需要如下自己写 dealloc 方法:

-(void)dealloc {
    [_foo release];
    [_bar release];
    [super dealloc];
}

ARC 下这种 dealloc 方法不再需要写了。OC++ 将帮你执行这两个 release。在对象销毁的过程中所有的 C++ 对象需要由 OC++ 执行销毁。当编译器发现一个对象包含了 C++ 对象时,它将生成一个名为 .cxx_destruct 的方法。ARC 依赖此方法并在其中发出所需的清理代码:

- (void) dealloc {
   CFRelease(_coreFoundationObject);
   free(_heapAllocatedMemoryBlob);
}

ARC 会自动生成销毁代码 不再需要我们自己写 dealloc 方法。这将大大降低了项目中源代码的大小以及减少模板代码。

覆写内存管理方法
在 ARC 之前,覆写内存管理方法是可行的。例如单例的 release 方法经常被覆写因为单例是不会被释放的。这在 ARC 下面是非法的因为这么做会影响 ARC 对对象生命周期的理解。而且因为方法是不允许被覆写的,ARC 在执行 retain、release 或者 autorelease 时,不适用 OC 的消息机制,而是使用了运行时的 C 函数。这意味着 ARC 能够对立马被持有的自动释放的对象进行优化。

小结

  • 自动引用计数使得开发者不需要再担忧大部分内存管理的问题。使用 ARC 能够减少类里面的模板代码。
  • ARC 对对象的内存管理主要通过在合适的时候增加 retain 和 release 方法。变量的限定词能用于指示变量的内存管理的语义,以前 retain 和 release 需要手动安排。
  • 方法名可用于表示返回的对象的内存管理的语义。ARC 巩固了这一点并使得命名规则不得不被遵守。
  • ARC 只管理 OC 对象。这意味着 CoreFoundation 对象不被管理,这时候就需要 CFRetain/CFRelease 函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值