什么是ARC?ARC就是Automatic Reference Counting的简称,翻译过来就是自动引用计数,自动引用计数(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时,这个对象会自动销毁,释放它所持有的内存。
CFigure1
图 1.
我们也可以用一个工厂方法来创建ObjC对象。这样就标记了这个对象是自动释放的,将他的指针加到自动释放池(如图 2)。我们可以通过autorelease消息来实现对alloc/init的对象达到发送release消息的目的。
CFigure2
图 2.
在每一个事件周期中,自动释放内存池都会去检测自身的对象指针集。当它发现超出其作用域并且引用记数为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)。
CFigure3
图 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语句加入其中。
CFigure4
图 4.
举例来说,如果对象被分配内存并处于一个方法当中,它会在这个方法的结尾处获得一个release语句。如果是一个类属性,它的release语句会加入到类的dealloc方法中。如果这个对象是用来返回的或者它是一个容器对象,它会加入一个autorelease语句。又如果这个对象是弱引用,把它放在一边不管它。
前端也为非局部对象插入保留语句。他更新所有使用@property指示符声明的访问器。他添加了对父的dealloc的调用,并且他报告任何的显式的管理调用和任何不清晰的所有权。
在优化阶段,Clang 使修改过的代码遵从加载平衡。它统计每一个对象保留和释放的调用,然后把它们缩减到优化的最小值。这避免了过度的保留和释放,能够在性能上产生影响。
为了描述, 看看在列表1中的实例代码。

列表 1

@class Bar;
@interface Foo
{
@private
NSString *myStr;
}
@property(readonly) NSString *myStr;
- (Bar *)foo2Bar:(NSString *)aStr;
- (Bar *)makeBar;
//...
@end
@implementation Foo;
@dynamic myStr;
– (Bar *)foo2Bar:(NSString *)aStr
{
Bar *tBar;
if (![self.myStr isEqualToString:aStr])
{
myStr = aStr;
}
return ([self makeBar]);
}
- (Bar *)makeBar
{
Bar *tBar
//...
//... conversion code goes here
//...
return (tBar);
}
//...
@end

在这里,我展示了一个没有任何保留和释放消息的ObjC类。它有一个私有的属性myStr,它是一个NSString(第5行)的实例.它声明了一个只读的getter, 也命名为myStr(第7行).它定义了一个修饰符foo2Bar和一个内部函数makeBar(18-36行)。这个类也使用@class指示符导入了类Bar的header(第1行)。
列表2列出了相同简单的经过ARC处理后的代码。
列表2
@class Bar;
@interface Foo
{
@private
NSString *myStr;
}
@property (readonly) NSString *myStr;
- (Bar *)foo2Bar:(NSString *)aStr;
- (Bar *)makeBar;
//...
@end
@implementation Foo;
@dynamic myStr;
– (Bar *)foo2Bar:(NSString *)aStr
{
Bar *tBar;
if (![self.myStr isEqualToString:aStr])
{
[aStr retain];
[myStr release];
myStr = aStr;
}
return ([self makeBar]);
}
- (Bar *)makeBar
{
Bar *tBar
//...
//... conversion code goes here
//...
[tBar autorelease];
return (tBar);
}
//...
- (void)dealloc
{
[myStr release];
[super dealloc];
}
@end

类接口并没有被改变。但是它的foo2Bar被新加入了两行代码。一个语句发送一个release消息给属性myStr(行24)。另一个发送一个retain消息给参数aStr(行25)。makeBar函数在返回之前发送一个autorelease消息给局部变量tBar,并将tBar作为返回值。最后,ARC重写了类的dealloc方法。在这个方法中,它释放了属性myStr(行44)并且调用父类的dealloc方法(行45)。如果一个dealloc方法已经存在,ARC会将它的代码作适当更新。
因为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创建项目本身。
CFigure5
图 5.
CFigure6
图 6.
为了验证ARC是否被开启,在项目窗口中的组和文件面板中选择项目图标 (Figure 7).从Target Setting工具条中点击Build Settings。,然后点击那个工具条下边的All。向下滚动并且定位到设置组Apple LLVM computer 3.0 — Language。 查找条目Objective-C Automatic Reference Counting — 它的值应该是Yes.
CFigure7
图 7.
如果你想将一个已经存在的iOS工程转化成ARC要怎样做呢?打开这个工程进入Xcode并且点击Edit菜单。从Refactor子菜单(Figure 8),选中Convert to Objective-C ARC…,会弹出另一个帮助对话框,进行相应的操作步骤。
CFigure8
图 8.
点击Next按钮可以看到一个构建目标列表(图9)。选中一个目标并点击Precheck来开始重构工程。如果重构失败,Xcode会警示用户,并在工程窗口中列出相应的重构错误。
CFigure9
图 9.
如果重构成功,Xcode将进入对比模式(如图 10)。这个帮助对话框分割成三个面板。左面的面板展示了涉及到的文件,默认是被选中的。中间的面板展示了源文件,右边的面板展示了修改后的文件。检查建议的改变并且点击Save提交他们或者点击Cancel放弃修改。无论如何请确保有一个代码的备份。否则,你将不能把工程文件还原到ARC之前的状态。
CFigure10
图片 10.
如果你想从ARC中移除一些工程文件会怎么样?如果是这样,在Xcode是对比模式时取消文件旁边复选框的选中状态(看图10)。如果这个文件可以加入到重构工程中,在文件面板的分组中选中工程图标。点击Target Settings 面板中Build Phases,并且向下滚动找到Compile Sources分组(如图 11)。点击分组头选择要移除的文件。然后点击Compile Flags列并且在模态对话框中输入-fno-objc-arc。点击Done设置标记,它将在每个文件中出现。
CFigure11
图片 11.

ARC指导方针

当从头开始写ObjC代码的时候,你应该确保代码与ARC兼容。代码必须给ARC完成工作所必要的线索。否则,他可能在错误的地方插入代码,或者更糟,他可能在代码中报告错误。
这些是写ARC兼容代码的指导方针。这些Apple官方文档和博客文章被编辑放在文章的最后。

  • 让ARC决定如何以及何时保留或者释放对象。
  • 如前所述,ARC前端捕获所有的显式的保留和释放消息。 他将把它们当做错误报告,并且你将不得不手动删除它们以便让项目可以编译。因此永远也不要发送保留或者释放消息给ObjC对象。永远不要使用retainCount消息去检查对象的保留状态,因为那个消息不再是可靠的。另外,不要使用@selector指令去调用对象的保留和释放方法。
    至于@property访问器,不使用assign,copy, retain特性来声明他们。让ARC决定给每一个访问器什么特性。

  • 让ARC管理自动释放池和它的对象。
  • 再一次声明,ARC前端补货所有的显式的自动释放消息。确保从你的ObjC代码中排除掉这些消息。也不要使用NSAutoreleasePool去创建池。相反,使用@autorelease指令标记池的位置和的范围。这告诉前端插入为ARC优化过的创建和释放池的代码。
    考虑一下在列表3中的例子代码。
    列表 3
    void main()
    {
    NSArray *tArgs = [[NSProcessInfo processInfo]
    arguments];
    unsigned tCnt, tLmt = [tArgs count];
    @autorelease
    {
    for (tCnt = 0; tCnt < tLmt; tCnt++) { @autorelease { NSString *tDat; NSString *tNom; tNom = [tArgs objectAtIndex:tCnt]; tDat = [[[NSString alloc] initWithContentsOfFile:tNom] autorelease]; // Process the file, creating and // autoreleasing more objects } } // Do more tasks, creating and autoreleasing // more objects } // Do whatever cleanup is needed exit (EXIT_SUCCESS); }

    在这里,main()入口函数使用了两个自动释放池。第一个池出现在函数的开始位置(第7行),他一直保持活动状态直到函数结束(第28行). 任何被这个函数标记为自动释放的对象都进入到这个池中。
    第二个池出现在每一个for循环的开始处(11-23行). 当循环结束时,他释放这个池并且创建一个新的。在每一次循环中被标记为自动释放的对象进入到这个池中,而不是第一个池。

  • 显式的声明任何弱的和非保留引用。
  • ARC假定所有的对象引用都是强的。当然,有一些例外, 最好的例子就是窗口视图(图 12)。这个视图保持每一个子视图和他包含的widget的强引用。换句话说,每一个子视图和widget仅仅保持一个对父视图的弱引用。
    CFigure12
    图 12.
    然而,两个对象之间相互强引用是可能的。这种情况称为循环引用。考虑列表 4中的例子。
    列表 4
    @interface FooLink
    {
    FooLink *fooNext;
    FooLink *fooPrev;
    NSData *fooData;
    NSInteger fooID;
    }
    @property(readwrite, assign) FooLink *nextLink;
    @property(readonly) FooLink *prevLink;
    - (NSData *)getData;
    - (void)addToLink:(NSData *)aDat;
    - (FooLink *)searchForLink:(NSInteger)anID;
    - (BOOL)hasLink:(NSInteger )anID;
    //...
    @end

    类FooLink实现了一个双向链表,每个节点拥有一个NSData对象。属性fooNext和fooPrev都指向另外的FooLink对象。由于两个指针形成了强引用,ARC将不会销毁这个对象。
    为了阻止这种事发生,声明其中一个属性为弱引用。在列表 5中,我在属性fooPrev前面加了一个__weak指令(第四行)。这样,当fooNext指向空并且FooLink对象超出范围时,ARC可以安全的给这个对象发送一个release消息。
    列表 5
    @interface FooLink
    {
    FooLink *fooNext;
    __weak FooLink *fooPrev;
    NSData *fooData;
    NSInteger fooID;
    }
    @property(readwrite, assign) FooLink *nextLink;
    @property(readonly) FooLink *prevLink;
    - (NSData *)getData;
    - (void)addToLink:(NSData *)aDat;
    - (FooLink *)searchForLink:(NSInteger)anID;
    - (BOOL)hasLink:(NSInteger )anID;
    //...
    @end

    __weak指示符既可以声明弱引用,也可以声明空引用。另一个指示符__unsafe_unretained 声明弱引用,而不是空引用。如果你计划自己处理空引用,那么使用这个指示符。 但是一定要去处理,否则你将最终会内存泄露。
    当然还有另外的一些编译器指示符,但是前面两个是你经常会用到的。

  • 避免转换ObjC对象到C指针。
  • 指向ObjC对象的指针式是一般的typeid的指针。为了把他们当做一个C-routine的输入,你可能可能转换这个指针如列表6 (3-9行)所示。这里,我使用了工厂方法stringWithString创建了一个NSString实例。接着我映射它到一个整形指针,并且把它传递给doFoo。
    列表 6
    // Recasting an ObjC object pointer
    {
    id tStr;
    int *tPtr;
    tStr = [NSString stringWithString:@"foobar"];
    tPtr = (int *)tStr;
    doFoo(tPtr);
    }
    // Using a CFObject
    {
    CFStringRef tStr;
    tStr = CFSTR("foobar");
    doFoo(tStr);
    }

    但是,转换阻止ARC正确的管理对象。ARC将不知道什么时候去保留对象,什么时候释放对象。此外转换指针可能最终指向一个无效的对象。因此,不要转换,使用核心的基础APIs 去创建C兼容的对象。(14-17行).
    避免将ObjC对象作为C结构体的字段。
    在一个C结构体中我们能够使用ObjC对象作为字段。列表7的示例中的struct就有两个这样的字段(4-5行):一个是NSString实例,另一个是NSURL实例。如果 ObjC对象是自定义的,我们可能我们可能使用@def指示符渲染他的属性的可见性。
    列表 7
    // A C struct with ObjC fields
    typedef struct FooStruct
    {
    NSString *fooName;
    NSURL *fooPath;
    int fooCount;
    char *fooData;
    } Foo;
    // A C struct with CoreFoundation fields
    typedef struct BarStruct
    {
    CFStringRef *barName;
    CFURLRef *barPath;
    int barCount;
    char *barData;
    } Bar;
    // The C struct rewritten as an ObjC class.
    @interface BarClass
    {
    NSString *fooName;
    NSURL *fooPath;
    int fooCount;
    char *fooData;
    }
    //...property accessors goes here
    @end

    但是就是在这里,ARC将不能够管理在结构体中的ObjC对象。它可能不能识别对那些对象的引用也不能为那些对象提供正确的保留,释放和自动释放语句。 同样的,使用核心的基础APIs去为结构体创建对象(13-16行)。或者将这个结构体重写为一个简单的ObjC类(20-28行)。

  • 自己管理所有的C和CF对象。
  • 就像垃圾回收,ARC忽略所有使用stdlib和核心基础APIs创建的对象。他将不插入被用来创建和释放结构体或者联合体的malloc()和free()调用。也不会为CF对象插入CFRetain()和CFRelease()调用。
    确保自己添加提供这些调用。小心平衡得为每一个malloc()调用一个free()并且为每一个CFRetain()调用一个CFRelease()。也确保避免在同一个对象上调用两次free()或者CFRelease()。

    总结

    自动引用计数是一个创新的管理在MacOS X 10.7和iOS 5上的ObjC对象的方法。它远离了显式的保留,释放以及自动释放消息,并且他降低了潜在的内存泄露和空指针。它提供了优化的自动释放池,避免了垃圾回收那样的性能开销。他的行为在两个平台是一致的。但是,ARC有很多方面太复杂以至于不能在这篇文章中覆盖到。为了学习了解更多的与ARC相关的东西,请查看下边的参考列表。