第二章:ARC入门
iOS 5 中最具颠覆性的变化是增加了自动引用计数(Automatic Reference Counting),简称 ARC。 ARC 是新的 LLVM 3.0 编译器的一个特性,它完全消除了所有 iOS 开发人员都因爱生恨的手动内存管理。
在自己的项目中使用 ARC 非常简单。 除了不再调用 retain,release 和 autorelease 之外,你可以像往常一样继续编程。 这基本上就是 ARC 的全部了。
启用自动引用计数功能后,编译器会自动在程序的正确位置插入 retain,release 和 autorelease。 你不必再为此担心,因为编译器会为你做。 这让我很惊喜。 实际上,使用 ARC 简单到你现在可以停止阅读本教程。
但是,如果你仍然对 ARC 持怀疑态度,也许你不相信它总是正确,或者你认为它会比手动内存管理慢一些 —— 那么继续读下去。 本教程的其余部分将驱散这些迷雾,并向你展示如何处理在项目中启用 ARC 的一些不那么直观的后果。
另外,我们将为你提供将不使用 ARC 的应用程序转换为使用 ARC 的实践经验。 你可以使用这些相同的技术将你现有的 iOS 项目转换为使用 ARC,从而节省大量内存麻烦!
运行原理
你可能已经熟悉了手动内存管理,基本上这样工作:
-
如果你需要持有一个对象,你需要保留(retain)它,除非它已经被你保留。
-
如果你不再使用一个对象,你需要释放(release)它,除非它已经被你释放(使用autorelease)。
作为一个初学者,你可能会有一段艰难的时间,期间这个概念一直萦绕在脑海,但过了一段时间后,它变成了第二天性,现在你总能正确地用 release 来平衡你的 retain。 除非你忘了这样做。
手动内存管理的原则并不难,但很容易犯错误。 而这些小错误可能会有可怕的后果。 要么你的应用程序在某个时候会崩溃,因为你将一个对象释放了太多次,你的变量指向的数据不再有效,要么你的内存耗尽,因为你没有充分释放对象, 它们永远存在下去。
Xcode 的静态分析器对于发现这些问题非常有帮助,但 ARC 更进一步。 它通过自动为你插入适当的 retain 和 release 来完全避免内存管理问题!
认识到 ARC 是 Objective-C 编译器的一个特性是非常重要的,因此当你构建你的应用程序时,所有的 ARC 相关的都会发生。 ARC 不是一个运行时特性(除了一小部分,就是弱指针系统),也不是从其他语言中了解的垃圾回收(garbage collection)。
所有 ARC 所做的只是在编译时插入 retain 和 release 到你的代码中,就在你会自己添加它们的地方 —— 或者至少是你应该添加它们的地方。 这使得 ARC 与手动托管代码一样快,有时甚至更快,因为它可以私下执行某些优化。
指针保存对象存在
你需要学习的 ARC 新规则非常简单。 通过手动内存管理,你需要保留一个对象以保持存在。 这不再是必要的,你只需要创建一个指向对象的指针。 只要有一个指向对象的变量,该对象就停留在内存中。 当指针获取新的值或不再存在时,关联的对象被释放。 所有变量都是如此:成员变量,合成属性,甚至局部变量。
从所有权的角度来看这是有道理的。 当你做到下面的事情时,
NSString *firstName = self.textField.text;
firstName 变量成为指向文本框内容中的 NSString 对象的指针。 firstName 变量现在是该字符串对象的所有者。
Beginning ARC-1.jpg一个对象可以有多个所有者。 在用户更改 UITextField 的内容之前,它的 text 属性也是字符串对象的所有者。 有两个指针保持相同的字符串对象存在:
Beginning ARC-2.jpg过了一会儿,用户将在文本框中输入新的东西,它的 text 属性现在指向一个新的字符串对象。 但原来的字符串对象仍然拥有一个拥有者(firstName 变量),因此保留在内存中。
Beginning ARC-3.jpg只有当 firstName 获得一个新的值,或者超出作用域 —— 因为它是一个局部变量而方法结束了,或者因为它是一个成员变量而它所属的对象被释放了 —— 所有权才会过期。 字符串对象不再拥有任何所有者,其保留计数将降至 0,并且对象被释放。
Beginning ARC-4.jpg我们将诸如 firstName 和 textField.text 之类的指针称为“强”指针,因为它们保持对象存在。 默认情况下,所有成员变量和局部变量都是强指针。
同样也存在“弱”指针。 弱指针变量仍然可以指向对象,但它们不会成为所有者:
__weak NSString *weakName = self.textField.text;
Beginning ARC-5.jpg
weakName 变量指向和 textField.text 属性指向的同一个字符串对象,但它不是所有者。如果文本框的内容发生变化,那么字符串对象不再拥有任何所有者并被释放:
Beginning ARC-6.jpg发生这种情况时,weakName 的值自动变为 nil。即所谓“归零”弱指针。
注意这个特性非常方便,因为它可以防止弱指针指向已被释放的内存。这类事情曾经造成很多漏洞 —— 你可能听说过“野指针”或“僵尸” —— 但是由于这些归零的弱指针,这已经不再是问题!
你可能不会频繁使用弱指针。当两个对象具有父子关系时,它们最有用。父类会有指向子类的强指针 —— 因此“拥有”孩子 —— 但为了防止所有权循环,子类只有指向父类的弱指针。
委托模式是一个例子。你的视图控制器可以通过强指针拥有一个 UITableView。表格视图的数据源和委托指针指回视图控制器,但是是弱指针。我们稍后会详细讨论。
Beginning ARC-7.jpg注意以下代码并不是很有用:
__weak NSString *str = [[NSString alloc] initWithFormat:...];
NSLog(@"%@", str); // will output "(null)"
字符串对象没有所有者(因为 str 是弱指针),所以该对象在创建后立刻被释放。当你这样做时,Xcode 会发出警告,因为它可能不是你所期望发生的事情("Warning: assigning retained object to weak variable; object will be released after assignment")。
你可以使用 __strong 关键字来表示变量是一个强指针:
__strong NSString *firstName = self.textField.text;
但是因为默认情况下变量是强指针,这就有点多余了。
属性也可以是强属性和弱属性。属性的写法如下:
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, weak) id <MyDelegate> delegate;
ARC 非常棒,它会消除大量混乱的代码。你不再需要考虑何时 retain 以及何时 release,而只需考虑对象如何相互关联。你需要问自己的问题是:谁拥有什么?
例如,以前不可能编写这样的代码:
id obj = [array objectAtIndex:0];
[array removeObjectAtIndex:0];
NSLog(@"%@", obj);
在手动内存管理下,从数组中删除对象将使 obj 变量的内容无效。只要不再是数组的一部分,该对象就会被释放。使用 NSLog() 打印对象可能会导致应用程序崩溃。在 ARC 中,上面的代码按预期工作。因为我们把对象放入 obj 变量中,这是一个强指针,数组不再是该对象的唯一所有者。即使我们从数组中删除对象,该对象仍然存在,因为 obj 始终指向它。
自动引用计数也有一些限制。对于初学者,ARC 仅适用于 Objective-C 对象。如果你的应用程序使用 Core Foundation 或者 malloc() 和 free() ,那么你仍然有责任对其进行内存管理。我们将在后面的教程中看到这个例子。此外,某些语言规则会更加严格,以确保 ARC 能够始终正确地完成其工作。这些只是小小的牺牲,你获得的好处比你放弃的要多得多!
仅仅因为 ARC 负责在适当的地方为你 retain 和 release,并不意味着你可以完全忘记内存管理。因为强指针使对象保持存在,所以有些情况下仍然需要手动将这些指针设置为 nil,否则应用程序可能耗尽可用内存。如果你一直保留你所创建的对象,那么 ARC 将永远无法释放它们。因此,无论何时创建新对象,仍然需要考虑谁拥有该对象以及该对象应该存在多长时间。
毫无疑问,ARC 是 Objective-C 的未来。Apple 鼓励开发人员抛弃手动内存管理,并开始使用 ARC 编写新应用程序。它能保证更简洁的代码和更健壮的应用程序。使用 ARC,与内存相关的崩溃已成为历史。
但是因为我们正在进入从手动到自动内存管理的过滤阶段,所以你经常会遇到与 ARC 不兼容的代码,无论是在你自己的代码中还是第三方库。幸运的是,你可以在同一个项目中将 ARC 与非 ARC 代码结合使用,我将向你展示几种方法。
ARC 甚至可以很好地与 C++ 结合。除了一些限制外,你也可以在 iOS 4 上使用 ARC,这应该有助于加速对此的接受。
一个聪明的开发人员会尽可能地自动化完成工作,这正是 ARC 所提供的:自动完成你以前必须手动完成的编程琐事。对我来说,切换到 ARC 是想都不用想的。
应用程序
为了演示如何在实践中使用自动引用计数,我准备了一个简单的应用程序,我们将从手动内存管理转换为 ARC。应用程序艺术家(Artists)包含具有表格视图和搜索栏的单个屏幕。当你在搜索栏中输入内容时,该应用会调用 MusicBrainz API 搜索名称匹配的音乐人。
应用看起来像这样:
Beginning ARC-8.jpg用他们自己的话来说,MusicBrainz 是“一本开放的音乐百科全书,它收集音乐数据并向公众开放”。他们有一个免费的 XML 网络服务,你可以在你的应用程序中用到它。要了解有关 MusicBrainz 的更多信息,请访问 http://musicbrainz.org 查看他们的网站。
你可以在本章的文件夹中找到艺术家应用程序的初始代码。该项目包含以下源文件:
-
AppDelegate.h/.m:应用程序委托。没什么特别的,每个应用都有一个。它加载视图控制器并将其放入窗口中。
-
MainViewController.h/.m/.xib:应用程序的视图控制器。它有一个表格视图和一个搜索栏,并且完成大部分工作。
-
SoundEffect.h/.m:一个用于播放声音效果的简单类。当 MusicBrainz 搜索完成时,该应用程序会发出嘟嘟声。
-
main.m:应用程序的入口点。
另外,该应用程序使用两个第三方库。你的应用程序可能会使用一些自己的外部组件,学习如何使这些库同 ARC 和谐共存是有益的。
-
AFHTTPRequestOperation.h/.m:AFNetworking 库的一部分,可以轻松地执行对Web服务的请求。我没有包含完整的库,因为我们只需要这一个类。你可以在 https://github.com/gowalla/AFNetworking 找到完整的软件包。
-
SVProgresHUD.h/.m/.bundle:一个在搜索过程中显示于屏幕上的进度指示器。你可能以前没有见过 .bundle 文件。这是一种特殊类型的文件夹,其中包含 SVProgressHUD 使用的图像文件。要查看这些图像,请右键单击 Finder 中的 .bundle 文件,然后选择 Show Package Contents 菜单选项。有关此组件的更多信息,请参阅:https://github.com/samvermette/SVProgressHUD。
让我们快速浏览一下视图控制器的代码,以便对应用程序的工作原理有一个好的概念。MainViewController 是 UIViewController 的一个子类。它的 nib 文件包含一个 UITableView 对象和一个 UISearchBar 对象:
Beginning ARC-9.jpg