这篇文章还可以在这里找到 英语波兰语

iOS 5 ARC 入门-LMLPHP

Learn the ins and outs of ARC in iOS 5!

这是iOS
5 盛宴
中的第12篇教程! 这篇教程是我们的新书 iOS
5 By Tutorials
 的一个预览章节。 Matthijs Hollemans 是这个章节的作者 – 也是 iOS
Apprentice Series
 的作者。 希望你喜欢!

这篇文章发表自 iOS 教程团队成员 Matthijs Hollemans
一个经验丰富的 iOS开发者和设计师。

iOS 5 中最具争议的一个新特性就是 Automatic Reference Counting (自动引用计数), 或者简写为 ARC。 ARC 是 LLVM 3.0 编译器的一个新特性,它彻底抛弃了让所有 iOS 开发者由爱生恨的手动内存管理机制。

在你的项目中使用 ARC 非常简单。 除了不再需要调用 retain, release 和 autorelease, 你可以和平常一样的开发。 基本上如此而已。

在开启 Automatic Reference Counting 的情况下, 编译器将会自动地在你程序的正确位置插入 retain, release 和 autorelease 。你不再需要为这些事情操心了, 因为编译器已经为你做了这些事。 实际上, 使用 ARC 是如此的简单,你甚至都不需要读这篇教程。 ;-)

但是,如果你还对 ARC 有一些质疑 — 或许你不相信它能正确的处理所有事情, 或者你会认为这样会比手动内存管理执行的速度慢 — 那就继续阅读吧。 教程余下的部分将会解开它神秘的面纱, 并向你展示哪些在使用 ARC 时,如何处理那些不太直观的结果。

另外, 我们将会手把手的教你如何将一个没有使用 ARC 的应用转换成使用 ARC 的。 你可以用这些技术来将你现存的 iOS 项目转换成使用 ARC。 帮助你减少大量的内存处理问题!

它是怎么工作的

你大概已经熟悉如何手工管理内存了, 就像这样:

  • 如果你想保持一个对象可用,除非它已经被 retain 了,否则你就需要 retain 它。
  • 如果你不再需要一个对象了, 那么你就需要 release 它, 除非它已经被 release 了 (通过 autorelease)。

作为一个初学者, 你可能需要一个艰难的过程来了解这个概念, 但在一段时间之后, 它将成为你的第二习惯。 现在你必须总是正确的在 retain 和 release 之间取得平衡。 除非你忘了。

手工内存管理的原则并不难,但是它非常容易出错。 并且这些小错误可能会带来严重的后果。 过度的 release 一个对象并且你的变量指向了一个已经无效的地址,你的应用就会在某一时刻崩溃掉,或者如果你没有在该 release 的时候释放掉内存,你的应用就会将内存占满。

Xcode 中的静态分析是帮你找到这类问题的好帮手, 但是 ARC 做得更好。 通过自动地在合适的位置帮你插入 retain 和 release, 它完全避免了内存管理中的问题!

有一个重要的一点需要知道的,就是 ARC 是 Objective-C 编译器的一个特性, 所有 ARC 相关的处理都会发生在你构建你的应用的时候。 ARC 不是运行时特性(除了其中的一点, weak 指针系统)。 它也不是你在其他编程语言中所了解的垃圾回收机制。

ARC 所做的事情就是在你编译代码的时候,在那些你需要自己处理内存管理的地方,插入 retain 和 release。 这就使得 ARC 和手工内存管理的速度一样快, 有时还会更快一些, 因为它在后端进行了一些优化操作。

指针保持对象存在可用

你需要了解的 ARC 的新规则很简单。 在手工内存管理中, 你需要 retain 一个对象,来让他可用。 这不再需要了, 你所需要做的仅仅是让一个指针指向那个对象。 只要有变量指针指向那个对象, 那么它就会一直在内存中。

当这个指针指向了另外一个对象,或者不再存在的时候, 和它相关联的那个对象会被释放掉。 这对所有类型的变量都适用: 实例变量, 属性, 甚至局部变量。

用所有者的方式来想它,就更好理解了。 当你这样做时,

NSString *firstName = self.textField.text;

firstName 变量成为一个指向 NSString 对象的指针, 它指向了文本框中的内容。 firstName 变量现在就是这个字符串对象的所有者。

iOS 5 ARC 入门-LMLPHP

一个对象可以有多个所有者。 直到用户改变了 UITextField 中的内容之前, 它的 text 属性也是这个字符串对象的所有者。 有两个指针指向了同一个对象:

iOS 5 ARC 入门-LMLPHP

稍后,用户会在文本框中输入一些新的文字, 这时它的 text 属性指向了一个新的字符串对象。 但是最初的那个字符串对象还是有一个所有者(firstName 变量), 所以它仍然在内存中。

iOS 5 ARC 入门-LMLPHP

只有当 firstName 也得到一个新的值的时候,或者它超出了作用范围 — 因为它是一个局部变量并且方法到了结尾,或者它是一个实例变量并且拥有它的对象被释放了 — 这时所有关系就失效了。 这个字符串对象不再有任何所有者, 它的 retain count 减少到 0,这个对象被释放了。

iOS 5 ARC 入门-LMLPHP

我们称 firstName 和 textField.text 为 “strong” 类型的指针, 因为他们能保持对象的存活。 默认情况下,所有的实例变量和局部变量都是 strong 指针。

也还有一个 “weak” 指针。 weak 类型的变量也能指向一个对象,但是他们不能成为所有者:

__weak NSString *weakName = self.textField.text;

iOS 5 ARC 入门-LMLPHP

weakName 变量和 textField.text 属性指向了同一个字符串对象, 但它不是所有者。 如果文本框的内容改变了, 这个字符串对象就不再有任何所有者了,然后就被释放了:

iOS 5 ARC 入门-LMLPHP

这个时候, weakName 变量的值会自动变成 nil。 它也被叫做 “zeroing” weak 指针。

这个特性非常方便,因为它防止了指针指向了被释放的内存。 这种情况会导致很多 bug — 你可能听说过”悬空指针”或”僵尸”这样的术语 — 但多亏了 zeroing weak 指针, 这些不再是问题了!

你或许不会频繁的使用到 weak 指针。 他们大多在两个父子关系的对象上面比较有用。 父对象会有一个 strong 类型的指针指向子对象 — 因此它就”拥有”了子对象 — 但是为了防止所有关系循环, 子对象仅仅有一个 weak 指针指向父对象。

这样的一个例子是代理模式。 你的控制器拥有一个指向 UITableView 的 strong 指针。 Table View 的 datasource 和 delegate 反过来指向控制器,但用的是 weak 指针。 我们稍后会详细讨论这个。

iOS 5 ARC 入门-LMLPHP

下面这个非常有用:

__weak NSString *str = [[NSString alloc] initWithFormat:...];
 
NSLog(@"%@", str); // will output "(null)"

这个 string 对象没有任何所有者(因为 str 是 weak 的), 这个对象将会在创建后直接被释放掉。 Xcode 将会给你一个警告, 因为这或许不是你想要的结果 (“Warning: assigning retained object to weak variable; object will be released after assignment”)。

你可以用 __strong 关键字来表示一个变量是 strong 类型的指针:

__strong NSString *firstName = self.textField.text;

但是因为变量默认就是 strong 的,这样做是多余的。

属性可以是 strong 的,也可以是 weak 的。 属性的表示方法如下:

@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 变量, 它是一个 strong 指针, 数组不再是这个对象唯一的所有者。 即使我们从数组中把这个对象删除掉, 这个对象仍然有效, 因为 obj 还在指向它。

Automatic Reference Counting 也还有一些不足。 作为刚刚起步的特性, ARC 仅仅适用于 Objective-C 对象。 如果你的应用用到了 Core Foundation 或者 malloc() 和 free(), 你还需要负责内存管理。 我们将会在这篇教程后面的部分看到一些例子。 另外, 为了让 ARC 正确的工作,一些语言规则会变得更严格。 仅有小小的牺牲, 你得到的会比你付出的更多!

正因为 ARC 帮你在适当的位置处理了 retain 和 release, 但这并不代表你可以完全忘记内存管理机制。 因为 strong 指针能够保持对象存活, 仍然存在一些情况,你需要手动的把这些指针设置为 nil, 不然你的应用将会耗尽可用内存。如果你一直保持所有你创建的对象都存活,那么 ARC 将不能释放他们。 因此, 当你创建一个新对象的时候, 你还需要考虑谁拥有它,还有这个对象应该存活多久。

毫无疑问,ARC 将会是 Objective-C 的未来。 苹果鼓励开发者放弃手工内存管理,从而开始用 ARC 来写他们的新应用。 它能带来更简单的源代码和更健壮的应用。 在 ARC 中, 内存相关的崩溃已经成为了过去。

但是,因为我们成处于一个从手工向自动内存管理过渡的阶段, 你会经常碰到那些还不兼容 ARC 的代码, 不论是你自己的代码,还是第三方库的。 幸运的是,你可以将 ARC 和非 ARC 的代码共同用于一个项目,我将给你展示几个怎样实现的方法。

ARC 甚至能和 C++ 很好的结合。 只需要遵守很少的一些限制,你还可以在 iOS 4 中使用 ARC, 这个仅仅用于帮助加快它的采用进程。

一个聪明的开发者会试着尽可能的让他的工作自动化。 这正是 ARC 所提供的: 将你之前那些必须手动处理的初级编程工作自动化起来。 对我来说,这样的转变很容易。

应用

为了展示如何在实践中使用 Automatic Reference Counting。 我准备了一个简单的项目,我们将把它从手工内存管理转换到 ARC。 这个应用,Artists, 由一个Table View 和一个 Search Bar 组成的界面。 当你在Search Bar 中输入一些东西时, 这个应用调用 MusicBrainz API 来根据姓名搜索音乐家。

这个应用看起来是这样:

iOS 5 ARC 入门-LMLPHP

用他们自己的话说,MusicBrainz 是”一个开放的音乐百科全书, 为公众提供音乐的元数据”。 他们提供一个免费的 XML web service 来供你的应用调用。 如果像更多的了解 MusicBrainz, 可以去他们的网站看看http://musicbrainz.org.

下载这个教程的起始项目,
并用 Xcode 打开它。 这个项目包含下面的源文件:

  • AppDelegate.h/.m: 应用的代理。 这个没什么特别的,每个应用都有它。 它加载控制器,然后将它放到 window 上面。
  • MainViewController.h/.m/.xib: 应用的控制器。 它有一个 table view 和一个 search bar, 并且做了最多的工作。
  • SoundEffect.h/.m: 一个简单的类,用于播放特效声音。 当 MusicBrainz 搜索完成时,应用会发出一个小小的蜂鸣声。
  • main.m: 应用的入口。

另外, 这个应用用到了两个第三方库。 你的应用可能会用到一些外部组件, 这也正好可以学习如何用 ARC 来处理这些库。

  • AFHTTPRequestOperation.h/.m: 是 AFNetworking 库的一部分,可以让你简单的请求 web service。 我没有导入这个库的全部,
    因为我们只需要用到这一个类。 你可以在这里找到完整的包 https://github.com/gowalla/AFNetworking
  • SVProgresHUD.h/.m/.bundle: 当进行搜索的时候,它会在屏幕上显示一个进度指示器。 你以前可能还没见过 .bundle 文件。 这是一个特殊的目录,它包含了
    SVProgressHUD 需要用到的图片。 可以在 Finder 中通过右键点击 .bundle 文件,然后选择 “显示包内容” 来查看这些图片。 关于这个组件更多的信息,可以查看这里: https://github.com/samvermette/SVProgressHUD

让我们快速的浏览一遍控制器的代码, 这样你能全面的了解应用是如何工作的。 MainViewController 是 UIViewController 的子类。 它的 nib 文件包含了一个 UITableView 对象和一个 UISearchBar 对象:

iOS 5 ARC 入门-LMLPHP

Table View 显示了 searchResults 数组中的内容。这个指针初始值是 nil。 当用户进行了一次搜索, 我们用 MusicBrainz 服务器响应的数据来填充这个数组。 如果没有相关的搜索结果,这个数组将会是空的(但不是 nil), Table 上面会显示 “(Nothing found)”。 这些功能都是通过 UITableViewDataSource 的方法完成的:numberOfRowsInSection 和 cellForRowAtIndexPath。

实际的搜索过程是在 searchBarSearchButtonClicked 中进行的, 它是 UISearchBarDelegate 协议的一部分。

- (void)searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
 
{
 
[SVProgressHUD showInView:self.view status:nil
 
networkIndicator:YES posY:-1
 
maskType:SVProgressHUDMaskTypeGradient];

首先,我们创建了一个新的 HUD, 并且将他显示在 TableView 和 Search bar 的上面, 在网络请求完成之前, 阻止用户的任何输入:

iOS 5 ARC 入门-LMLPHP

然后我们创建 HTTP 请求的 URL。 我们用 MusicBrainz 的 API 来搜索艺术家。

NSString *urlString = [NSString stringWithFormat:
 
@"http://musicbrainz.org/ws/2/artist?query=artist:%@&limit=20",
 
[self escape:searchBar.text]];
 
NSMutableURLRequest *request = [NSMutableURLRequest
 
requestWithURL:[NSURL URLWithString:urlString]];

要搜索的文字通过 escape 方法进行 URL 编码: 保证我们的 URL 是有效的。 空格和其他特殊符号会转换成类似这样的形式: %20 。

NSDictionary *headers = [NSDictionary dictionaryWithObject:
 
[self userAgent] forKey:@"User-Agent"];
 
[request setAllHTTPHeaderFields:headers];

我们为 HTTP 请求添加了一个自定义的 User-Agent 头。 MusicBrainz API 需要它。 所有的请求都应该 “有一个合适的 User-Agent 请求头,以便用来标识发送请求的应用和版本。” 和你正在用的 API 配合好总是一个好主意, 所以我们像这样构建了一个 User-Agent 请求头:

com.yourcompany.Artists/1.0 (unknown, iPhone OS 5.0,
 
iPhone Simulator, Scale/1.000000)

()

(I took this formula from another part of the AFNetworking library and put it into the userAgent method in the view controller.)

MusicBrainz API 还有一些其他的规则。 客户端应用每秒只能发送一个请求到 web service 中, 否则它们的 IP 有可能被屏蔽。 这对我们的应用来说不是个大问题 — 用户不太可能做这么多次的搜索 — 所以我们不需要预防这类操作。

当我们创建完 NSMutableURLRequest 对象, 我们把它发给 AFHTTPRequestOperation 来处理:

AFHTTPRequestOperation *operation = [AFHTTPRequestOperation
 
operationWithRequest:request completion:^(NSURLRequest *request,
 
NSHTTPURLResponse *response, NSData *data, NSError *error)
 
{
 
// ...
 
 
 
}];
 
 
 
[queue addOperation:operation];

AFHTTPRequestOperation 是 NSOperation 的一个子类, 这也意味着我们可以把它添加到 NSOperationQueue(在 queue 变量上) 中, 并且它会进行异步处理。 因为用到了 HUD, 当正在请求数据的时候,应用忽略了所有的用户输入。

我们给 AFHTTPRequestOperation 指定了一个 block, 当请求完成的时候它会被调用。 在 block 中,我们首先检测请求是否成功(通过 HTTP 状态码 200)。 在这个应用中我们对为什么失败不感兴趣; 如果失败了,我们仅仅让 HUD 以一个”失败”动画消失。 注意 completion block 不一定会在主线程上执行, 所以我们需要将对 SVProgressHUD 的调用包装到 dispatch_async() 中。

          if (response.statusCode == 200 && data != nil)
 
{
 
. . .
 
}
 
else // something went wrong
 
{
 
dispatch_async(dispatch_get_main_queue(), ^
 
{
 
[SVProgressHUD dismissWithError:@"Error"];
 
});
 
}

现在开看看有趣的部分。 如果请求成功了, 我们创建 searchResults 数组,并且解析响应。 服务端返回的数据是 XML 格式的,所以我们用 NSXMLParser 来解析它。

               self.searchResults = [NSMutableArray arrayWithCapacity:10];
 
 
 
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
 
[parser setDelegate:self];
 
[parser parse];
 
[parser release];
 
 
 
[self.searchResults sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];

你可以在 NSXMLParserDelegate 的方法中找到解析 XML 的逻辑代码, 我们实际上仅仅查找了名称为 “sort-name” 的元素。 它们包含了艺术家的姓名。 我们将这些姓名以 NSString 对象的形式放入 searchResults 数组。 当 XML 解析完成时, 我们以字母表顺序对这个数组进行排序, 并且在主线程中更新界面显示:

dispatch_async(dispatch_get_main_queue(), ^
 
{
 
[self.soundEffect play];
 
[self.tableView reloadData];
 
[SVProgressHUD dismiss];
 
});

这些就是这个应用如何工作的。 它使用手工内存管理,并且没有用到任何 iOS 5 的特性。 现在,让我们把它转换到 ARC 吧。

自动转换

我们将要把 Artists 应用转换到 ARC。 简单来说, 我们不再需要调用 retain, release 和 autorelease 了, 但在一些特定情况下,我们还需要做一些特殊处理。

这里有三个方式能让你的应用做到 ARC 兼容:

  1. Xcode 有一个自动转换工具, 他可以迁移你的源代码。
  2. 你可以手工的转换这些文件。
  3. 你可以对那些你不想转换的文件禁用 ARC。 这对那些你不想混在一起的第三方库非常有用。

我们将在 Artists 应用中用到所有这些操作, 仅为了向你展示所有这些是如何工作的。 在这个部分, 我们将会通过 Xcode 的自动转换工具来转换源文件, 除了 MainViewController 和 AFHTTPRequestOperation。

在我们做这些事情之前, 你应该把项目拷贝一份,因为这个工具会覆盖原来的文件。 Xcode 会提供一个对源代码的预览界面, 但是为了防止丢失,我无论如何也会备份一份。

ARC 是 LLVM 3.0 的一个新特性。 你现有的项目很可能用的是老的 GCC 4.2 或者 LLVM-GCC 编译器, 所以首先要做的是将项目的编译器切换到新版本,看一看编译器是否在非 ARC 模式。 进入 Project Settings 界面,

选择 Artists target, 在 Build Settings 选项卡中的搜索框中输入 “compiler”. 这样可以过滤列表,展示出编译选项:

iOS 5 ARC 入门-LMLPHP

点选 Compiler for C/C++/Objective-C 选项,修改它为 Apple LLVM compiler 3.0:

iOS 5 ARC 入门-LMLPHP

在 Warnings 头中,还要把 Other Linker Flags 设置为 -Wall。 这样编译器将会检测所有会导致问题的情况。 默认情况下,这些警告消息是被关闭的,但是我发现总是把他们打开并且将每一个都看作是致命错误是很有用的。 换句话说, 如果编译器给出任何警告,我将会在继续其他工作之前修复它。 是否在你自己的项目中也这样做完全取决于你, 但是在转换到 ARC 的过程中, 我推荐你仔细看看编译器给出的每一个问题。

同样地, 也要在 Build Options 头中打开 Run Static Analyzer:

iOS 5 ARC 入门-LMLPHP

Xcode 现在将会在每次构建项目的时候运行静态分析。 这会让构建的速度稍微慢一点, 但是作为我们这种规模的应用来说,这不算什么。

让我们来构建一下应用, 看看新的编译器会给出什么问题. 首先我们用 Product -> Clean(或 Shift-Cmd-K) 进行一次清理。 然后按下 Cmd-B 来构建应用。 Xcode 应该不会收到任何警告。 如果你在将你自己的项目转换到 ARC, 并且收到了警告消息, 那么现在正是修复他们的时候。

让我们把编译器切换到 ARC 模式,并且再次构建应用。 我们收到了一大堆错误消息, 现在正好可以看看这些究竟是什么。

仍然在 Build Settings 屏幕中, 切换到 “All” 可以看到所有可用的设置(Basic选项仅仅显示最常用到的设置)。 搜索 “automatic”, 设置 Objective-C Automatic Reference Counting 选项为 YES。 设置一个项目范围的标记, 用来告诉 Xcode 你将要用 ARC 编译器来编译你的项目中所有的源文件。

iOS 5 ARC 入门-LMLPHP

再次构建应用, 你应该会看到一大堆错误:

iOS 5 ARC 入门-LMLPHP

很明显,我们要进行一些迁移! 大多数的错误都很明显,他们说的都是你不能再用 retain,release 和 autorelease 了。 我们可以完全手工的修正这些错误, 但是使用自动转换工具会更容易一些。 这个工具会用 ARC 模式来编译应用, 然后对每一个它遇到的错误的地方进行重写,直到项目不再报错。

在 Xcode 菜单中, 选择 EditRefactorConvert to Objective-C ARC.。

iOS 5 ARC 入门-LMLPHP

一个新的窗口会弹出来, 让你选择哪些部分是你想要转换的:

iOS 5 ARC 入门-LMLPHP

我们不希望转换整个项目, 只选择下面这些文件:

  • main.m
  • AppDelegate.m
  • SVProgressHUD.m
  • SoundEffect.m

这个对话框显示了一个小警告图标,用来指示这个项目已经使用了 ARC。 这是因为我们之前在 Build Settings 中开启了 Objective-C Automatic Reference Counting 选项, 所以自动转换工具会认为它已经是一个 ARC 项目了。 你可以忽略这个警告, 他不会对转换造成影响。

按下 Precheck 按钮来开始转换。 这个工具首先会检测你的代码是否处于足够好的状态来转换到 ARC。 我们之前用新的 LLVM 3.0 编译器成功的构建了我们的应用, 但是很明显这次不行。 Xcode 会给出如下错误:

iOS 5 ARC 入门-LMLPHP

它提示 “ARC readiness issues”, 我们应该开启 “Continue building after errors” 选项。 我们应该先开启这个选项。 打开 Xcode 的 Preferences 窗口 (在 Xcode 的菜单中), 进入 General 标签。 开启选项 “Continue building after errors”:

iOS 5 ARC 入门-LMLPHP

让我们再试一次。选择 EditRefactorConvert to Objective-C ARC 并且选中除了 MainViewController.m 和 AFHTTPRequestOperation.m 之外的所有源文件。 按下 Precheck 按钮。

iOS 5 ARC 入门-LMLPHP

很不幸, 我们再一次得到了一个错误。 和刚才不同的是这次编译器能够在转换之前找出我们需要修复的所有问题。 幸运的是, 只有一个错误:

iOS 5 ARC 入门-LMLPHP

你可能会在这个列表中看到更多的错我。 有时转换工具会提示那些不完全是 “ARC readiness” 性的错误。

我们这个问题的完整描述是:

Cast of Objective-C pointer type 'NSURL *' to C pointer type 'CFURLRef' (aka 'const struct __CFURL *') requires a bridged cast

在源代码编辑器中它看起来是这样的:

iOS 5 ARC 入门-LMLPHP

我会在后面更详细的讨论它, 但这里的代码想尝试着把 NSURL 对象转换成 CFURLRef 对象。 AudioServicesCreateSystemSoundID() 函数接受一个 CFURLRef 参数, 用来描述声音文件的位置, 但是我们给他的是一个 NSURL 对象。 CFURLRef and NSURL 是 “可互换的”, 这样可以在需要 CFURLRef 的地方使用 NSURL, 反过来也一样。

一般情况下, iOS 中基于 C 语言的 API 会用到 Core Foundation 对象 (这也是 CF 的意思), 而基于 Objective-C 的 API 用的是继承自 NSObject 的”真正的”对象。 有时你需要在这两者之间进行转换, 这也是 “可互换的” 对象所代表的意思。

然而, 当你使用 ARC 的时候,编译器需要知道应该怎样处理 “可互换的” 这些对象。 如果你在需要 CFURLRef 的地方使用了 NSURL, 那么谁负责在最后释放这些内存呢? 为了解决这个难题, 一系列的新关键字被引入进来:

__bridge, __bridge_transfer 和 __bridge_retained。 我们会在接下来的教程中深入的了解如何使用它们。

现在我们需要按照下面的方式修改源代码:

OSStatus error = AudioServicesCreateSystemSoundID((__bridge CFURLRef)
 
fileURL, &theSoundID);

预检测可能会给出不止这一个错误。 你可以安全的忽略它们, 上面对 SoundEffect.m 的修改是唯一一个我们要做的。 转换工具看起来对把什么当做 “ARC 先决条件问题” 也不是很确定。

让我们再运行一次转换工具 – EditRefactorConvert to Objective-C ARC。 这次预检测运行的没有任何问题, 我们将会得到这样的界面:

iOS 5 ARC 入门-LMLPHP

点击 Next 继续。 几秒钟之后, Xcode 将会为他要修改的所有文件和在这些文件上面进行的修改内容显示一个预览界面。 左边的面板显示的是修改过的文件,右边的面板显示的是原始文件。

iOS 5 ARC 入门-LMLPHP

一个好的习惯是浏览一下所有这些文件,确保 Xcode 不会弄乱任何东西。让我们从头到尾看一遍转换工具给我们展示的修改内容吧。

AppDelegate.h

@property (strong, nonatomic) UIWindow *window;
 
@property (strong, nonatomic) MainViewController *viewController;

App Delegate 有两个属性, 一个是 Window, 另一个是 View Controller。 这个项目没有包含 MainWindow.xib 文件, 所以这两个对象由 AppDelegate 在 application:didFinishLaunchingWithOptions: 方法中创建, 并存放到属性中以便进行内存管理。

这些属性的声明修改如下:

@property (retain, nonatomic)

修改为:

@property (strong, nonatomic)

strong 关键字和你想象的一样。 它告诉 ARC 这个属性对应的 synthesize 声明的实例变量,持有一个当前对象的强引用。 换句话说, window 属性包含了一个指向 UIWindow 对象的指针,并且作为这个 UIWindow 对象的所有者。 只要 window 属性还保留着它的值, 那么这个 UIWindow 对象就一直存活。 viewController 属性和 MainViewController 对象也是一样的。

AppDelegate.m

在 AppDelegate.m 中, 创建 window 和 view controller 对象的代码被修改了, 并且 dealloc 方法完全被删除了:

iOS 5 ARC 入门-LMLPHP

看看他们之间不同的地方,

self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
 
bounds]] autorelease];

和:

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
 
bounds]];

这是没问题的, 不再需要调用 autorelease 了。 创建 view controller 的代码也是一样。

self.viewController = [[[MainViewController alloc] initWithNibName:
 
@"MainViewController" bundle:nil] autorelease];

现在变成了:

self.viewController = [[MainViewController alloc] initWithNibName:
 
@"MainViewController" bundle:nil];

在使用 ARC 之前, 如果你像这样写代码,如果这个属性声明成 “retain” 类型的, 那么就会造成内存泄露:

self.someProperty = [[SomeClass alloc] init];

init 方法返回了一个被 retain 过的对象,又把它赋值给声明为 retain 的属性。 这也是为什么你必须要调用 autorelease, 用来和 init 方法中的 retain 进行平衡。 但是在 ARC 中,上面的代码是没问题的。 编译器足够的只能,可以发现它不应该被 retain 两次。

ARC 一个让我喜欢的地方是它完全不再需要写 dealloc 方法了。 当一个对象被释放时, 它的实例变量和属性也会自动跟着释放。 你不再需要写这些东西了:

- (void)dealloc
 
{
 
[_window release];
 
[_viewController release];
 
[super dealloc];
 
}

因为 Objective-C 自动的处理了这些问题。 事实上, 像上面这样写代码也是不可能的。 在 ARC 中,你是不允许调用 release 和 [super dealloc] 消息的。 你仍然可以实现 dealloc 方法 — 并且后面你会看到一个例子 — 但这里不再需要手动的释放你的实例变量。

自动转换工具不会帮你把 AppDelegate 从 NSObject 的子类转换为 UIResponder 的子类。 当你用新的 Xcode 模板来创建项目的时候, AppDelegate 类已经是 UIResponder 的子类了。 让他继承自 NSObject 也没什么坏处, 但是如果你愿意,你可以将它继承自 UIResponder:

@interface AppDelegate : UIResponder <UIApplicationDelegate>

Main.m

在手工内存管理的应用中, [autorelease] 方法和 “autorelease pool” 一同工作, 它用 NSAutoreleasePool 对象来表示。 甚至 main.m 中也有一个, 如果你直接操作线程, 那么必须为每个线程创建一个你自己的 NSAutoreleasePool。 有时,开发者还会将 NSAutoreleasePool 放到一个工作量很大的循环中, 用来保证循环中的自动释放对象不会占据很大的内存,并且每次循环完成后都会被清楚掉。

在 ARC 中 autorelease 依然存在,即使你不需要直接的调用 [autorelease] 方法。 每次你用不是以 alloc, init, copy, mutableCopy 或 new 开头的方法返回一个对象时, ARC 编译器会自动帮你调用 autorelease 方法。 这些对象还会在 autorelease pool 中被释放。 一个比较大的差别是 NSAutoreleasePool 被写成了一个新的语法, @autoreleasepool。

iOS 5 ARC 入门-LMLPHP

自动转换工具把我们的 main() 函数从这样:

NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
 
int retVal = UIApplicationMain(argc, argv, nil,
 
NSStringFromClass([AppDelegate class]));
 
[pool release];
 
return retVal;

转换成这样:

@autoreleasepool {
 
int retVal = UIApplicationMain(argc, argv, nil,
 
NSStringFromClass([AppDelegate class]));
 
return retVal;
 
}

不仅对我们开发人员来说,它更容易阅读,而且在它的内部也进行了很多修改, 让这个新的 autorelease pool 比之前的快很多。 除非你在之前自己的代码中用到了 NSAutoreleasePool, 在 ARC 中你几乎不用再关心 autorelease 了, 你需要把它替换成 @autoreleasepool 代码块。 转换工具会自动的帮你完成这个工作。

SoundEffect.m

这个文件没怎么修改, 只是删除了 [super dealloc] 的调用。 你不能再在 dealloc 方法中调用 super 了。

iOS 5 ARC 入门-LMLPHP

注意 dealloc 方法在这里仍然是必须的。 在你的大多数类中你可以直接忘掉 dealloc, 让编译器替你处理这些事情。 然而,有时候, 你需要手工的释放一些资源。 这个类就属于这种情况。 当 SoundEffect 被释放的时候, 我们需要调用 AudioServicesDisposeSystemSoundID() 方法来清理 sound 对象并释放它。

SVProgressHUD.m

这个文件是这几个里面修改最多的, 但都比较琐碎。

iOS 5 ARC 入门-LMLPHP

在 SVProgressHUD.m 的顶部,你需要找到称为 “类扩展” 的代码, @interface SVProgressHUD (), 这里有一些属性的声明。 如果你不熟悉类扩展, 他们其实和 Category 很像,但有一些特殊的能力。 类扩展的定义和 Category 很相似, 但是它在 () 之间没有任何名字。 类扩展可以带有属性和实例变量, 而 Category 没有这些, 你仅仅能在这个类的 .m 文件中引用它们。 (话句话说,你不能在其他类中引用这个类扩展) 。

类扩展最酷的地方是,它能让你在你的类中增加私有属性和方法。 如果你不想在 public @interface 中暴露一些属性和方法, 那么你就可以把它们放到类扩展中。 这也是 SVProgressHUD 做的事情。

@interface SVProgressHUD ()
 
 
 
...
 
@property (nonatomic, strong) NSTimer *fadeOutTimer;
 
@property (nonatomic, strong) UILabel *stringLabel;
 
@property (nonatomic, strong) UIImageView *imageView;
 
@property (nonatomic, strong) UIActivityIndicatorView *spinnerView;
 
...
 
 
 
@end

像我们之前看到的, retain 的属性现在变成了 strong。 如果你浏览一下预览视图, 你会看到其他的更改只是简单的删除掉 retain 和 release 语句。

实际的应用它

如果你对转换工具做的修改还满意, 按下 Save 按钮,让他生效。 Xcode 首先会问题是否希望在进行文件修改之前保存一下项目的快照:

iOS 5 ARC 入门-LMLPHP

你应该点 Enable。 如果你退回到原来的代码, 你可以在 Organizer 中找到项目的快照。

当 ARC 转换工具完成后, 按下 Cmd+B 来构建应用。 构建应该成功的完成。 但会有一些关于 SVProgressHUD.m 的新警告:

iOS 5 ARC 入门-LMLPHP

注意到在这个类中仍然用到了 dealloc 方法, 在这里停止了 timer 并且从 NSNotificationCenter 中注销了通知。 很明显,这些是 ARC 不能帮你做的。

包含警告的这行代码看起来是这样:

if(fadeOutTimer != nil)
 
[fadeOutTimer invalidate], [fadeOutTimer release], fadeOutTimer = nil;

现在是:

if(fadeOutTimer != nil)
 
[fadeOutTimer invalidate], fadeOutTimer, fadeOutTimer = nil;

工具删除了对 [release] 的调用, 但是它把变量还留在那。 一个单独的变量放在那没有任何用处,所以 Xcode 发出了警告。 这种情况自动转换工具是不能预知的。

如果你对逗号这种语法看到困惑, 那么你要知道,在 Objective-C 用逗号将多个表达式连接成一个语句是有效的。 上面是一个释放对象和设置相应的实例变量为空时的一个通用技巧。 因为所有事情都在一行代码中处理了。 所以不需要花括号。

你可以修改这行代码来消除警告:

if(fadeOutTimer != nil)
 
[fadeOutTimer invalidate], fadeOutTimer = nil;

从技术上说我们不需要这个 fadeOutTimer = nil; 在 dealloc 中, 对象会在被删除时释放它所有的实例变量。 但在其他的方法中, 当 timer 失效之后, 你必须设置 fadeOutTimer 为 nil。 如果你不这样做, SVProgressHUD 会长时间等待这个失效的 timer。

再次构建应用, 这次不会有任何警告。 转换成功了!

但是,等一下, 我们在转换时跳过了 MainViewController 和 AFHTTPRequestOperation。 他们是如何在编译过程中没有任何问题的? 当我们之前试着用 ARC 来构建项目时, 在这些文件中会出现很多错误。

答案很简单: 转换工具为这两个源文件关闭了 ARC 功能。 你可以在 Project Settings 的 Build Phases 标签中看到他们:

iOS 5 ARC 入门-LMLPHP

我们之前通过在 Build Settings 中设置 Objective-C Automatic Reference Counting 选项为 YES, 在项目全局开启了 ARC。 但是你也可以告诉编译器一些例外,让它忽略一些指定的文件, 通过 -fno-objc-arc 标记。 Xcode 将会为这些文件关闭 ARC。

因为,不可能期待开发者一次性的将整个项目迁移到 ARC 中, Apple 的员工们让 ARC 和非 ARC 的代码可以在同一个项目中工作。 提示: 一个简单的方式是,直接通过转换工具迁移那些你想转换的文件,然后它会自动为其余的文件增加 -fno-objc-arc 标记。 你也可以手工的添加这些标记, 但当你有很多文件的时候,这会很无聊。

05-18 05:08