英文原文:Automatic Reference Counting on iOS
自动引用计数(ARC)是在MacOS X 10.7与iOS 5中引入一项新技术,用于管理Objective-C中的对象。它废弃了显式的retain、release和autorelease消息,而且在两个平台的表现一致。 由于有限的内存以及手持设备续航能力的限制,iOS应用程序中的Objective-C对象的管理一直颇有挑战性。为了处理这些问题,苹果提出了一个方案——“自动引用计数”(ARC)。在这篇文章中,我会把ARC和显式的retain/release以及垃圾回收进行对比,除此之外还会展示如何在一个iOS项目中使用它,并且探讨一些ARC的使用准则。 读者应当拥有Objective-C和Xcode IDE的使用经验。 |
通过消息传送来实现 首先,我通过显示的消息传送来管理ObjC对象。我用alloc和init消息来创建对象(如图 1)。我发送retain消息来保持一个对象,并且发送release消息来释放掉它。通过alloc/init创建的ObjC对象会有一个内部的值为1的引用计数。retain消息会使这个引用计数加1,然而,release消息会使这个引用计数减1.当这个引用计数为0时,这个对象会自动销毁,释放它所持有的内存。 我们也可以用一个工厂方法来创建ObjC对象。这样就标记了这个对象是自动释放的,将他的指针加到自动释放池(如图 2)。我们可以通过autorelease消息来实现对alloc/init的对象达到发送release消息的目的。 | |
在每一个事件周期中,自动释放内存池都会去检测自身的对象指针集。当它发现超出其作用域并且引用记数为1的对象,它就会通过发送一个release消息释放这个对象。当不想释放这个对象时,我们可以发送一个或多个retain消息给这个对象。否则,我们必须让这个对象发送的retain和release消息一样多,才能将它释放。 显式发送消息的方式仍然是iOS应用程序中管理ObjC对象的一种有效方法。它一般不会花费很多精力,可以很容易的定位bug,同时拥有性能好的特点。 在另一方面,显式发送消息的方式很容易导致出错。当retain和release消息不相等时,它会导致内存泄露或EXC_BAD_ACCESS错误。另外,显式地释放一个已经释放了的对象也会导致EXC_BAD_ACCESS错误。并且,对象容器(如数组,集合等)可能并不会对它包含的引用记数大于1的对象运行该对象的构造函数。 |
使用垃圾回收管理 MacOS X 10.5 (Leopard) 给我们另一个管理ObjC对象的方法— 垃圾回收。这里,每一个Cocoa应用程序得到自己的作为次级线程运行的收集服务 (Figure 3)。 这个服务标识所有在一起动就创建的根对象,然后跟踪每一个后来创建的对象。它检查每一个对象的范围以及对根对象的强引用。如果对象有这些特性,那么收集服务将它保留下来(用蓝色标记)。否则,它使用一个finalize消息释放这个对象(用红色标记)。 |
收集服务是保守的。当必须保证高性能时,它可以被中断,甚至暂停 。它是一个分代的服务。他假定最新被创建的对象寿命最短。 通过类NSGarbageCollector来使用收集服务。使用这个类,我能够禁用这个服务或者改变它的行为。我甚至能指定新的根对象或者重置服务本身。 垃圾回收移除了显式的保留和释放消息的需要。它能够降低野指针也能够防止空指针。换句话说,它需要所有定制的ObjC对象被更新。清除代码必须进入到finalize方法,而不是 dealloc方法。ObjC对象也必须向他的父发送一个finalize消息。 接下来,收集服务需要知道何时一个对象的引用是弱的。否则,他假设所有的引用都是强的。这可能导致循环引用和内存泄漏。这个服务也忽略使用withmalloc()创建的对象:那些对象应该被手动释放或者使用Cocoa函数NSAllocateCollectable()来创建。 最后,这个服务依然会导致性能受到影响,尽管他是保守的。这就是垃圾回收在iOS上缺席的原因。 |
进入 ARC ARC是一种全新的方式,它拥有很多垃圾回收机制的优点,但却没有那样的性能损耗。 从内部来看,ARC并不是一项运行时的服务。实际上它是由新的Clang front-end提供的两段过程。图4显示了这两段过程。在front-end段时,Clang检查每个预处理文件的对象和属性。然后它跟据一些固定的规则将正确的retain,release和autorelease语句加入其中。 ![]() 图4. 举例来说,如果对象被分配内存并处于一个方法当中,它会在这个方法的结尾处获得一个release语句。如果是一个类属性,它的release语句会加入到类的dealloc方法中。如果这个对象是用来返回的或者它是一个容器对象,它会加入一个autorelease语句。又如果这个对象是弱引用,把它放在一边不管它。 |
前端也为非局部对象插入保留语句。他更新所有使用@property指示符声明的访问器。他添加了对父的dealloc的调用,并且他报告任何的显式的管理调用和任何不清晰的所有权。 在优化阶段,Clang 使修改过的代码遵从加载平衡。它统计每一个对象保留和释放的调用,然后把它们缩减到优化的最小值。这避免了过度的保留和释放,能够在性能上产生影响。 为了描述, 看看在Listing One中的实例代码。 Listing One
|
列表2列出了相同简单的经过ARC处理后的代码。 列表2
|
因为ARC独自决定ObjC对象如何被管理,它省掉了开发类代码的时间。它阻止了任何野指针和空指针。它甚至能够基于文件来禁用。最后一个特征让程序员可以复用被证明是稳定的遗留代码。 但是Clang编译器被构建在LLVM 3.0中,它只能在Xcode 4.2或者更新的版本中获得.对于ARC的优化的运行时支持也仅仅出现在MacOS X 10.7 (Lion) 和 iOS 5.0。在iOS 4.3中通过粘合代码使用ARC是可能的。在后来提供的不使用任何弱指针的二进制文件中使用ARC也是可能的。 然后,ARC只对ObjC代码起作用。对于PyObjC和AppleScriptObjC代码没有任何效果。但是,它的确影响那些桥接PyObjC,ASOC类到Cocoa的底层ObjC对象。也值得注意的是,一些第三方的框架在使用ARC开启的编译的时候也可能会导致问题 。请确保在更新版本时联系框架的开发者。 |
为ARC做准备 在一个iOS项目中有两个方法支持ARC。一个是使用ARC-enabled的模板创建项目。另一个是使用Xcode改写一个现存的项目。 假设你想开始一个新的iOS项目。启动Xcode,从文件菜单里选择新项目。在新建对话框中 (Figure 5),选择一个项目模板(在这个例子中,单视图项目),点击Next查看项目选项。在界面的字段中输入项目名称,公司ID,以及类前缀。 然后勾选Use Automatic Reference Counting (Figure 6). 点击Next查看项目位置。设置位置和源代码仓库 (可选),然后点击Create创建项目本身。 为了验证ARC是否被开启,在项目窗口中的组和文件面板中选择项目图标 (Figure 7).从Target Setting工具条中点击Build Settings。,然后点击那个工具条下边的All。向下滚动并且定位到设置组Apple LLVM computer 3.0 — Language。 查找条目Objective-C Automatic Reference Counting — 它的值应该是Yes. |
如果你想将一个已经存在的iOS工程转化成ARC要怎样做呢?打开这个工程进入Xcode并且点击Edit菜单。从Refactor子菜单(Figure 8),选中Convert to Objective-C ARC...,会弹出另一个帮助对话框,进行相应的操作步骤。 图8 点击Next按钮可以看到一个构建目标列表(图9)。选中一个目标并点击Precheck来开始重构工程。如果重构失败,Xcode会警示用户,并在工程窗口中列出相应的重构错误。 图9 |
如果重构成功,Xcode将进入对比模式(如图 10)。这个帮助对话框分割成三个面板。左面的面板展示了涉及到的文件,默认是被选中的。中间的面板展示了源文件,右边的面板展示了修改后的文件。检查建议的改变并且点击Save提交他们或者点击Cancel放弃修改。无论如何请确保有一个代码的备份。否则,你将不能把工程文件还原到ARC之前的状态。 如果你想从ARC中移除一些工程文件会怎么样?如果是这样,在Xcode是对比模式时取消文件旁边复选框的选中状态(看图10)。如果这个文件可以加入到重构工程中,在文件面板的分组中选中工程图标。点击Target Settings 面板中Build Phases,并且向下滚动找到Compile Sources分组(如图 11)。点击分组头选择要移除的文件。然后点击Compile Flags列并且在模态对话框中输入-fno-objc-arc。点击Done设置标记,它将在每个文件中出现。 |
ARC指导方针 当从头开始写ObjC代码的时候,你应该确保代码与ARC兼容。代码必须给ARC完成工作所必要的线索。否则,他可能在错误的地方插入代码,或者更糟,他可能在代码中报告错误。 这些是写ARC兼容代码的指导方针。这些Apple官方文档和博客文章被编辑放在文章的最后。
|
在这里,main()入口函数使用了两个自动释放池。第一个池出现在函数的开始位置(第7行),他一直保持活动状态直到函数结束(第28行). 任何被这个函数标记为自动释放的对象都进入到这个池中。
第二个池出现在每一个for循环的开始处(11-23行). 当循环结束时,他释放这个池并且创建一个新的。在每一次循环中被标记为自动释放的对象进入到这个池中,而不是第一个池。
|
然而,两个对象之间相互强引用是可能的。这种情况称为循环引用。考虑列表 4中的例子。 列表 4
类FooLink实现了一个双向链表,每个节点拥有一个NSData对象。属性fooNext和fooPrev都指向另外的FooLink对象。由于两个指针形成了强引用,ARC将不会销毁这个对象。 为了阻止这种事发生,声明其中一个属性为弱引用。在列表 5中,我在属性fooPrev前面加了一个__weak指令(第四行)。这样,当fooNext指向空并且FooLink对象超出范围时,ARC可以安全的给这个对象发送一个release消息。 列表 5
|
__weak指示符既可以声明弱引用,也可以声明空引用。另一个指示符__unsafe_unretained 声明弱引用,而不是空引用。如果你计划自己处理空引用,那么使用这个指示符。 但是一定要去处理,否则你将最终会内存泄露。 当然还有另外的一些编译器指示符,但是前面两个是你经常会用到的。
|
但是,转换阻止ARC正确的管理对象。ARC将不知道什么时候去保留对象,什么时候释放对象。此外转换指针可能最终指向一个无效的对象。因此,不要转换,使用核心的基础APIs 去创建C兼容的对象。(14-17行).
|
总结 自动引用计数是一个创新的管理在MacOS X 10.7和iOS 5上的ObjC对象的方法。它远离了显式的保留,释放以及自动释放消息,并且他降低了潜在的内存泄露和空指针。它提供了优化的自动释放池,避免了垃圾回收那样的性能开销。他的行为在两个平台是一致的。但是,ARC有很多方面太复杂以至于不能在这篇文章中覆盖到。为了学习了解更多的与ARC相关的东西,请查看下边的参考列表。 |