lincode +

iOS 5 的 ARC 初探 (一)

[iOS 5 的 ARC 初探 (一)][1]

2011-12-18 By lincode

iOS 5 中最大的变化应该就是自动引用计数器,简称 ARC。ARC 是新的 LLVM 3.0 编译器的新功能,使得 iOS 开发人员摆脱了令人憎恨的手动内存管理。

在你的项目中使用 ARC 是极其简单的。你和往常一样编程,除去你不再使用 retain,release 和 autorelease。基本上这就是所有了。

开启自动引用计数器,编译器会自动在你的代码的正确位置插入 retain, release 和 autorelase。你无需再关心这些,因为编译器为你做了。这真是酷毙了。实际上,使用 ARC 是如此简单以至于你现在就可以停止阅读这个教程了。;-)

但是,如果你对 ARC 仍然持怀疑态度--也许你并不信任它能总是正确,或者你认为它可能会不明的比你自己来做内存管理要慢--那么你可以继续阅读。教程余下的部分将打消这些秘密并向你展示如何处理一些在项目中使用了 ARC 的隐性的后果。

另外,我们还将介绍一些把一个非 ARC 应用转成使用 ARC 的实用经验。你可以可以使用这些技巧将你的已经存在的 iOS 项目转成 ARC 项目,以节省你大量时间。

##如何工作 你可以已经熟悉手动内存管理,基本上它如此工作:

手动管理内存的原则并不困难但是很容易犯错。这些小错误能造成可怕的后果。要么你的应用因为过度释放对象或者你的变量指向一段不再有效的数据而崩溃,要么你会因为没有充分地释放对象而造成内存泄漏使得他们永远驻留于内存之中。

Xcode 地静态分析器对于找到这些类型的问题有很大的帮助,但是 ARC 则更进一步。它通过自动插入合适的 retain 和 release 完全避免了内存管理问题。

认识到 ARC 是一个编译器功能是很重要的,因为所有 ARC 动作都发生在你编译你的项目的时候。ARC 不是运行时功能(除了一小部分,弱指针系统),也不是其他语言中的垃圾收集。

ARC 所做的只是在编译时在你的代码中插入 retain 和 release。插入的位置就是你自己来做内存管理时应该插入的位置。这使得 ARC 的速度和手动内存管理一样快,甚至由于一些特定的隐藏的优化,它有时还会快一些。

##保持对象存在的指针 你需要为 ARC 学习的新规则相当的简单。手动内存管理时,你需要 retain 一个对象以保持其存在。这不再是必须的,所有你必须做的只是生成一个指针指向某个对象。只要一个变量指向一个对象,对象就存在于内存之中。当指针得到一个新值或者不再存在了,所关联的对象就被释放了。这对所有的变量都是有效的,包括实例变量,通过 synthesized 生成的属性,甚至局部变量。

想一下所有权问题是有意义的,特别是当你做如下事情时:

NSString *firstName = self.textField.text;

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

alt Pointers1

一个对象可以有不止一个所有者。直到用户更改 UITextField 的内容,它的 text 属性都是字符串对象的所有者。有两个指针指向同一个字符串对象,使其保持存在。

alt Pointers2

稍后,用户将在文本域打几个新字符,文本域的 text 属性现在指向一个新的字符串对象。但是原来的字符串对象仍然有一个所有者(变量 firstName)因此它仍然在驻留于内存之中。

alt Pointers3

只有当 firstName 也得到一个新值,或者超出了可视域-因为这是一个局部变量而方法已经结束了,或者因为它是一个实例变量而其所属的对象已经被释放了-即所有权过期了。这个字符串对象不再有任何所有者,它的 retain 计数器降为0,对象被释放

alt Pointers4

我们称如 firstName 和 textFeld.text 这样的指针为“强”,因为他们保持对象存在。所有实例变量和局部变量 都是强指针。

也有“弱”指针。弱指针变量仍然可以指向对象但不能成为对象的所有者。

__weak NSString *weakName = self.textField.text;

alt Pointers5

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

alt Pointers6

当这发生时,weakName 的值自动变为 nil。它被称为“零值”弱指针。

注意这是很方便的因为它不然弱指针指向一个被释放的内存区域。这类事情曾引起很多 bugs -你可能已经听闻过“悬摆指针”(“dangling”)或“僵尸”(“zombies”)这样的称谓了-但是由于零值指针这都不再是问题了!

你可能不会经常用到弱指针。在对象有父子关系时,它们很有用。父亲有一个指向儿子的强指针-因而拥有了儿子-但是为了避免循环拥有,儿子只有指向父亲的弱指针。

一个例子是代理模式。你的视图控制器(view controller)可能通过强指针而拥有一个 UITableView 的实例。UITableView 的实例列表视图(table view)的数据源和代理通过弱指针指向视图控制器。我们将在后面详述。

alt Pointers7

注意如下代码是没有用的:

__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 是强指针,数组不再是对象的唯一拥有者。甚至我们将对象从数组中删去,对象也仍然存在因为变量 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++。加上很少的限制(这些限制仅仅只是有利于 ARC 的使用),你可以在 iOS 4 上使用 ARC。

明智的开发者会尽量尝试使自己的工作自动化,这正是 ARC 所提供:使得以前重复艰苦的编程工作变得自动化。对于我而言,不用想便会转向 ARC。

##应用

为了展示如何在实践中使用自动引用计数,我准备了一个简单得应用。我们将把这个使用手动内存管理的应用转换为 ARC。应用名为,Artists,只包含一个带搜索框和列表的屏幕。当你在搜索框内输入某些字符时,应用通过 MusicBrainz API 搜索名字匹配的音乐家。

这个应用看起来是这样的:

alt App

用他们自己的话来说,MusicBrainz 是一个“开放的收集和公开音乐数据的音乐百科全书”。他们有免费的 XML web service,你可以在自己的应用中使用。要更多了解 MusicBrainz,请访问 http://musicbrainz.org.

访问下载本文档的初始项目,在 Xcode 中打开。项目包含如下文件:

另外,应用使用了两个第三方库。你的应用可能会使用一些额外第三库的组件,有必要学习如何使这些库很好地和 ARC 工作。

让我们快速浏览一下视图控制器地代码,以获得应用如何工作地大致了解。MainViewController 是 UIViewController 的子类。它的 nib 文件包含了一个 UITableView 对象和一个 UISearchbar 对象:

alt Nib

列表显示了数组 searchResults 的内容。指针初始是 nil。当用户执行一个搜索,我们用户从 MusicBrainz 服务器返回的结果填充数组。如果这里没有搜索结果,数组就是空的(但不是 nil),同时列表会说:“什么也没找到”。这通常都发生在 UITableViewDataSource 的方法:numberOfRowsInSection 和 numberOfRowsInSection 中。

实际的搜索从方法 searchBarSearchButtonClicked 开始,该方法是协议 UISearchBarDelegate 的一部分。

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

首先,我们创建了一个新的 HUD 并将它显示在列表和搜索框上面,阻止任何用户输入,直到网络请求结束:

alt HUD

然后我们为 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];

我们家了定制的 User-Agent HTTP 请求报头。MusicBrainz API 要求这样。所有的请求都应该有正确 User-Agent 报头以区分不同的应用和应用版本发出了请求。遵守你在使用的 API 的要求是个好主意,所以,我们总是构建如下 User-Agent 报头:

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

(我从 AFNetworking 库的另外一部分获得了这个公式,并把它放入视图控制器的 userAgent 方法。)

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 (一个队列变量) ,并会被异步处理。因为 HUD,应用忽略了请求发生过程中的所有的用户输入。

我们给了 AFHTTPRequestOperation 一个闭包,闭包将在请求完成时触发。在闭包内,我们先检查请求是否成功(HTTP 状态码为 200)。在这个应用中,我们对请求为何失败并不感兴趣;我们仅仅告知 HUD 以一个表示 “error” 的动画结束。注意闭包不是必然执行的在主线程因此我们需要使用 dispatch_async() 包裹对 SVProgressHUD 的调用。

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 解析完成le,我们将结果以字母表排序,然后在主线程中刷新屏幕:

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

应用就是如此工作的。它使用手动内存管理,没有用任何 iOS5 的特殊功能。现在让我们开始 ARC。

##自动转换 我们将要把 Artists 应用转换为 ARC。基本上这意味着我们将去除所有的 retain,release 和 autorelease 调用,但是我们仍然有一些地方需要特别的注意。

这有三个方法可以使你的项目兼容 ARC:

我们将在 Artists 应用中使用所有这些选项,以向你展示它们是如何工作的。在这一节中,我们将使用 Xcode 的自动转换工具转换除了 MainViewController 和 AFHTTPRequestOperation 以外的所有文件。

在我们做这些之前,你应该保存一份项目的拷贝,因为工具将覆盖原文件。Xcode 提供了会提供保存源文件快照的功能,但是保险起见,我们仍然应该做备份。

ARC 是 LLVM 3.0 编译器的新功能。你的已存在的项目可能是使用旧的 GCC4.2 或者 LLVM-GCC 编译器,所以最好首先将项目的设置改为新的编译器,并且看看新编译器能否在非 ARC 模式下正确编译。来到项目设置屏,选择 Artists target 和在 Building settings 下搜索 “compile”。这将过滤出和编译相关的选项:

alt Compiler

点击 “Compiler for C/C++/Objective-C” 将其改变为 Apple LLVM compiler 3.0:

alt LLVM

在 LLVM GCC 4.3 - Warnings 栏目下,将 Other Linker Flags 选项设置为 -Wall。编译器将检查所有可能发生问题的状况。缺省情况下,很多警告都被关闭了,但是我发现得到所有的警告并把它们视为致命错误是很有用的。换句话说,如果编译器给出警告,我会首先订正警告再继续。这是某些你在自己的项目上事情,这完全取决于你。但是,我建议你在转换为 ARC 期间好好看一下编译器给出的警告。

同一原因,也可以将 Build Option 栏目下的 Run Static Analyzer 选项开启:

alt Run-Static-Analyzer

编译器将在每次编译应用时跑分析器。这会时编译过程慢一点,但对于这个大小的应用这一点很难察觉。

让我们用新编译器编译应用看看会出什么问题。首先 clean 一下项目 Product -> Clean menu option (或者 Shift-Cmd-K)。然后按 Cmd-B 去编译应用。Xcode 应该不会给出错误和警告,太叼了。如果你要将你自己的应用装换为 ARC,同时又收到了警告信息,那么现在就是订正他们的时候了。

仅仅是为了好玩,我们将编译器切换到 ARC 模式,并再次编译应用。我们将收到一大堆错误信息,看看这些究竟是什么是很有用。

在 Building Settings 屏幕,切换到 “ALL” 以看到所有的设置(Basic 只能看到常用设置)。搜索 “automatic” 并将 “Objective-C Automatic Reference Counting” 选项设为 YES。这是个项目级别的标识,告知 Xcode 你希望用 ARC 编译器编译你项目中所有的源文件 。

alt Enable-ARC

再次编译应用。叼,你应该看到了无数错误。

alt Errors

很明显,我们有一些移植要做了!大部分错误是很明显的,它们说你不再能使用 retain,release 和 autorelease。我们可以手工订正这些错误,但是用自动转换工具来做也相当简单。工具将在 ARC 模式下编译应用并且在遇到错误时,改写源代码,直到编译通过为止。

在 Xcode 的菜单中,选择 Edit\Refactor\Convert to Objective-C ARC。

alt Convert-to-ARC

一个新窗口将显示让你选择你地应用哪些部分需要转换:

alt Select-targets-to-convert

为了教学需要,我们并不需要包括整个项目,所以只选择如下几个文件:

对话框显示了一个警告图标,意思是项目已经使用了 ARC。这是因为我们早些时候已经在 Building settings 中开启了 Objective-C Automatic Reference Counting 选项,使得转换工具认为这已经是一个 ARC 项目了。你可以忽略这个警告,它不会影响转换。

按 Precheck 按钮开始转换。工具先检查你的代码是否在一个足够好的状态以进行 ARC 转换。我们已经用了新的 LLVM 3.0 编译器成功编译了项目,但是显然还不够好。Xcode 给出以下错误信息:

alt Cannot-convert-error

这是说 “ARC 准备就绪问题”,需要我们启动 “Continue building after errors”选项。我们马上这么做。打开 Xcode Preference 窗口 (Xcode 的 menubar)到 General 选项卡。开启 Continue building after errors 选项:

alt Continue-building-after-errors

让我们再试一次。选择 Edit\Refactor\Convert to Objective-C ARC 并选择除了 MainViewController 和 AFHTTPRequestOperation.m 以外的所有源文件。按 Precheck 按钮:

alt Cannot-convert-error-2

很不幸,还是有错误信息。和之前不同的地方再这次编译器可以识别出所有我们在进行转换之前需要订正的问题。很幸运,只有一个:

alt Precheck-issues

(你可能有比上面显示出来的更多的错误。有时,转换工具可以会指出一些和 “ARC 准备就绪” 无关的问题。)

问题的完整描述是:

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

在编译器里看起来就像这样:

alt Cast-error

我们接着将描述关于这里的更多细节,源代码试图将一个 NSURL 对象强制转换为一个 CFURLRef 对象。函数 AudioServicesCreateSystemSoundID() 需要一个用于描述声音在哪里的 CFURLRef 对象,我们却给了一个 NSURL 对象。CFURLRef 和 NSURL 是相通可互换的,也就是说在使用 CFURLRef 对象的地方也可以使用 NSURL 对象,反过来也可以。

在 iOS 中的 C 语言 API 常常会使用 Core Foundation Objects (这就是为何有前缀“CF”),同时 Objective-C 语言 API 则会使用从 NSObject 继承而来的“正真”的对象。有时,你需要在两者之间做转换,这就需要“相通可互换”(toll-free bridging)技术。

可是,但你使用 ARC 模式时,编译器需 要知道如何处理这些相通可互换对象。如果你用 NSURL 对象替代 CFURLRef,那么最后谁负责释放对象?为了解决这个难题,我们引进一系列关键字:__bridge, __bridge_transfer 和 __bridge_retained。我们将在后面的教程中更深入地介绍如何使用它们。

现在,我们需要把代码改成这个样子:

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

预检查会给出更多的错误,不会仅限于这一个。忽略它们是很安全的,之前在 SoundEffect.m 的改变是你唯一需要做de改变。转换工具看起来每次关于“ARC 准备就绪问题”的检查结果并不一致。

让我们再进行一次转换-Edit\Refactor\Convert to Objective-C ARC。这次预检查运行得没有任何问题,你可以看到如下的屏幕:

alt Precheck-successful

点击 Next 继续。几秒种后,Xcode 将显示出所有要改变的文件和文件中何种改变的预览。左手边显示改变后的文件,右手边显示源文件。

alt Review-Changes

浏览这些文件以确保 Xcode 不弄糟任何东西会是个好主意。让我们浏览以下这些转换工具将要执行的变化。

##AppDelegate.h

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

应用委托(AppDelegate)有两个属性,一个是视窗,一个是主视图控制器。这个项目没有使用 MainWindows.xib 文件,所以两个对象由应用委托自己在 application:didFinishLaunchingWithOptions: 中创建,并存储于属性中以进行简单的内存管理。

这个属性从这样:

@property (retain, nonatomic)

变为这样:

@property (strong, nonatomic)

strong 关键字意味着其字面的意思。它告诉 ARC 属性背后的实例变量持有一个指向对象的强引用。换句话说,windows 属性包含一个指向 UIWindows 对象的指针,而且扮演 UIWindows 对象的拥有者。一旦 windows 属性拥有了值, UIWindows 对象就保持存活。viewController 属性和 MainViewController 对象也是同样的情况。

##AppDelegate.m

在 AppDelegate.m 中,创建视窗和视图控制器的对象已经变化了,dealloc 方法被完全去掉了

alt Changes-AppDelegate

看清楚这一行:

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

和这一行的区别:

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

不再需要调用 autorelease。在创建视图控制器的行是相同的情况。

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 方法返回一个保留过的(retained)对象。在赋值给属性时又会保留一次对象。这就是为什么你必须使用 autorelease,以平衡在 init 方法中调用的 retian。但是 ARC 模式下,以上代码没有问题。编译器足够聪明可以找出这个不应该做两次 retain。

我喜欢 ARC 的一件事情是在大部分时候都完全不需要写 dealloc 方法。当对象被释放时,实例变量和属性将自动被 调用 release。你不再需要写:

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

因为现在 Objective-C 自动处理这些。实际上,甚至是不可能再写上面的代码了。在 ARC 下你不被允许调用 release,和 []super dealloc]。你仍然可以实现 dealloc-你可以在后面看到一个例子-但是不再可以手动释放实例变量了。

转换工具没有做的一些事情是是将 AppDelegate 从 NSObject 的子类变为 UIResponder 的子类。当你使用 Xcode 的模榜创建一个新的应用时,AppDelegate 现在已是 UIResponder 的子类了。将其保留为 NSObject 的子类,看起来也没有什么伤害。但如果你希望的话,你可以使其继承 UIRespondor,代码如下:

@interface AppDelegate : UIResponder <UIApplicationDelegate>

##Main.m

在手动内存管理的应用中,方法 autorelease 和表示 NSAutoreleasePool 对象的”autorelease pool”一同工作。每一个 main.m 都有 autorelease pool。如果你要直接和线程工作,你就必须为每个线程创建你自己的NSAutoreleasePool。有时,开发者也在循环中为每个循环过程创建自己的 NSAutoreleasePool,以保证的在循环中创建的对象被调用过 autorelease 同时可以被及时释放,而不占用太多内存。

Autorelease 和 ARC 密不可分,即使你从不直接在对象上调用 autorelease 方法。任何时候你返回一个使用非 alloc,init,copy,mutableCopy,和 new 开始的方法创建的对象,ARC 编译器将自动为你在对象上调用 autorelease。这些对象仍然会在 autorelease pool 结束时被释放。很大的不同是,之前的 NSAutoreleasePool 对象被一个新语法 @autoreleasepool 代替了。

alt Review-Changes-main

转换工具将我们的 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 的速度比以前要快很多。你确实需要注意 ARC 下的 autorelease,除非你在代码使用 @autoreleasepool 块代替了 NSAutoreleasePool 。这个转换工具应该会自动将你完成这些。

##SoundEffect.m

这个文件没有太多改变,仅仅要删除 [super dealloc]。你不再被允许在 dealloc 中调用父类的 dealloc。

alt SoundEffect

注意,dealloc 函数在这里仍然需要。在你的大多数类中,你可以简单地忘记 dealloc 而让编译器照顾他们。但有时,你需要手动释放某些资源。这个类就是这种情况。当 SoundEffect 对象被释放时,我们仍然需要调用 AudioServicesDisposesSystemSoundID() 以清理声音对象,dealloc 是最佳的释放位置。

##SVProgressHUD.m

这个文件是所有文件中变化最大的,但又是很琐屑的。

alt SVProgressHUD

在 SVProgressHUD.m 的顶部,你将可以找到名为”类扩展”的部分,@interface SVProgressHUD(),这里有若干个属性声明。如果你对类扩展不熟悉,他们和目录类似只是他们多了一些特殊的能力。类扩展的声明看起来象目录,但是在()括号中间没有名字。类扩展可以有属性和实例变量,这是目录所没有的,但是你只可以在自己的 .m 文件中使用他们。(换句话说,你不能使用其他类的类扩展。)

这个关于类扩展很酷的一点是他们允许你为自己的类添加私有属性和方法。如果你不象在你的公共 @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 先会问你在改变这些文件之前是否要保存项目的快照:

alt Snapshots

你尽可能按 Enble。如果你需要恢复到原来的代码,你可以在项目的 Origanizer 窗口下找到这些快照。

在 ARC 转换工具完成之后,按 Cmd+B 编译应用。编译应该能够完全正确的完成,当时在 SVProgressHUD.m 中有几个新的警告:

再次注意到,dealloc 方法仍然在类中使用到了,在这个例子中,dealloc 中代码停止了计时器并且在 NSiNotificationCenter 注销了几个通知。当然,这里 ARC 没有为你做任何事情。

出现警告的代码曾是这样的:

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

现在是这样的:

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

工具去除了 release 的调用,但是将变量留下了。一个变量本身,不做任何事情。因此 Xcode 给出了警告。这看起来似乎转换工具并没有完全达到预期。

如果你被逗号搞糊涂了,那么要知道在 Objective-C 中,用逗号将多个表达式连接为一条语句是合法的。上面的招数在事发对象,将对象设为 nil 中是普遍的语法。因为,所有的事情都发生在一条语句中,并不需要花括号。

为了避免警告,你可以改变以下行如这样:

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

技术上说,我们不需要在 dealloc 中,做 fadeOutTimer = nil;因为在对象被删除时,它会自动释放它自己的实例变量。但是在其他方法中,计时器可能已指向非法地址,所以,你最终需要将 fadeOutTimer 置为 nil。如果你不这样做,SVProgressHUD 将一直挂着一个非法的的 NSTimer 对象。

再次编译应用,现在应该没有警告了。转换完成!

但是等一分钟… 转换中,我们跳过了 MainViewController 和 AFHTTPRequestOperation。他们在编译中为什么没有出现问题呢?当我们早些时候在 ARC 开启时编译项目时,这两个文件错误百出。

答案很简单:转换工具在这两个文件上禁止了 ARC。你可以在 Build Phases 选项页中看到项目设置:

alt Snapshots

我们可以在项目层面启动 ARC,在 Building 设置中将 Objective-C Automatic Reference 置为 YES。但是,我们也可以有例外,使用 -fno-objc-arc 标识告知编译器在特定文件上忽略 ARC。Xcode 将在编译这些文件时关闭 ARC。

因为,期望开发者一次性将整个项目转换为 ARC 是不合理的,Apple 的人允许在一个项目中混用 ARC 和非 ARC 代码。小贴士:一个简单方法做到 ARC 和 非ARC 混用是,用转换工具转换那些你想转换的文件,再让它为剩下的文件添加上 -fno-objc-arc 标识。你可以手工添加,但显然当你有很多文件不需要 ARC时,这是很烦人的事情。

##迁移中的痛苦

我们的转换进行得很顺利。我们只需在 SoundEffect.m 上做一个变化(插入 __bridge)然后让工具做剩下的事情。

可是,比较以前的编译器,LLVM 3.0 编译器对于 ARC 模式要求更为严格,以至于你可能会在预检时,陷入一些额外的问题。在工具接管之前,你可能必须更多地编辑你的代码。这里有一些你可能会遇到的问题的简便参考,以及一些关于如何解决这些问题的建议:

“Cast … requires a brigded cast”

这个错误我们之前已经看到过的。当编译器自己不能找出如何转型时,它希望你能插入 __bridg modifier。这里另外有两个的 bridge 类型,__bridge__transfer 和 __bridge__retained,使用哪一个取决于你试图做什么。在 关于”Toll free bridge”的章节,有更多关于这两个 bridge 类型的介绍。

###”向前向声明类型的实例发送消息”

如果你有一个类,让我叫假设 MyView 是 UIView 的子类,你调用它的一个方法或者使用它的一个属性,然后你必须 #import 这个类的定义。这通常可以使你的代码通过编译,但不是总是如此。

例如,你在 .h 文件中添加了一个前向声明宣告 MyView 是一个类

@class MyView;

让后在你的 .m 文件中你做了类似的事:

[myView setNeedsDisplay];

以前这可能可以通过编译而且良好工作,即使没有 #import 语句。在 ARC 下,你总是必须显式地添加引入

#import "MyView.h"

###”Switch case 需用{}明确生命周期”

在写如下代码时,你可能会遇到错误:

switch (X)
{	
    case Y:
    	NSString *s = ...;
	    break;
}

这不再被允许。如果你在 case 语句中声明一个新的指针变量,你必须用花括号将其括起来:

switch (X)
{	
    case Y:
    {
    	NSString *s = ...;
	    break;
	}
}

现在变量的作用域就清楚了,这正是 ARC 需要知道的,从而可以在正确的时间释放对象。

###“在 NSAutoreleasePool 区域内声明的”

你可能会有如下创建自己的 autorelease 池的代码:

NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
 
// . . . do calculations . . .
 
NSArray* sortedResults =  [[filteredResults sortedArrayUsingSelector:@selector(compare:)] retain];

[pool release];
return [sortedResults autorelease];

装换工具需要将它们转变为如:

@autoreleasepool
{
  	// . . . do calculations . . .       
  		NSArray* sortedResults = [filteredResults sortedArrayUsingSelector:@selector(compare:)];	
}
return sortedResults;

但是这不再是合法代码了。变量 sortedResults 被声明在 @autoreleasepool 的作用域内,因而在作用域外是不可访问的。为了解决这个问题,你需要将变量声明移到创建 NSAutoreleasePool 之前:

NSArray* sortedResults;
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
…

现在转换工具可以正确重写你的代码了。

###”ARC 禁止在结构体(structs)或者联合(unions) 中使用 Objective-C 对象”

ARC 的一个限制是你不能在 C 结构体中放 Objective-C 对象。如下代码不再是合法的:

typedef struct
{
	UIImage *selectedImage;
	UIImage *disabledImage;
}
ButtonImages;

建议你使用 Objective-C 类来代替这些结构体。我们将在后面更多地讨论这个,并给出解决方法。

也许还有其他预检错误,但这里提到的是最常见的。

注意 :如果在一个项目里多次使用自动转化工具,它的行为可能会显得有点古怪。如果你不能转化所有文件,而留有一些如上面提到的没有检查到的地方,那么,在你试图转化剩下的文件时,检查工具可能不会做任何事情。我的建议是你只执行一次转化工具,而不是多次执行转化工具。

##接下来去哪儿?

继续阅读系列文章第二篇,将涉及手动转化到 ARC,与 Core Foundation 相关的 ARC, 弱属性等等。

如果你有任何关于 iOS5 中 ARC 的问题,欢迎在后面的论坛留言!


原文连接http://www.raywenderlich.com/5677/beginning-arc-in-ios-5-part-1

点击查看评论

Blog

Opinion

Project