ARC 问答

原文:http://www.mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html

byMike Ash  

 

概念
" Clangstatic analyzer "是一个非常有用的查找代码中内存管理错误的工具。我在查看这个分析器的输出时经常会想,“既然你能找出错误,为什么就不能修正错误呢?”

实际上,这就是ARC的作用。编译器中包含了内存管理规则,但只能简单地由它自己来调用,无法帮助程序员查找错误。

ARC介于自动垃圾回收(GC)和手动内存管理之间。就像垃圾回收,ARC让程序员不再需要书写retain/release/autorelease语句。但它又不同于垃圾回收,ARC无法处理retaincycles。在ARC里,如果两个对象互相强引用(strong references)将导致它们永远不会被释放,甚至没有任何对象引用它们。

因此,尽管ARC能免去程序员大部分内存管理问题,但仍然要程序员自己避免retaincycles或手动打断对象之间的retain循环。ARC和苹果的垃圾回收之间还有一个重要的不同:ARC不是强制的。而对于苹果的垃圾回收,要么整个程序都使用,要么都不用。也就是说在app中的所有O-C代码,包括所有的苹果框架和所有的第3方库必须支持垃圾回收,才能使用垃圾回收。相反,ARC和非ARC代码可以在一个app中和平共处。这使得将项目可以零星地迁移到
ARC 而不会像垃圾回收起初遇到的各种兼容性和稳定性的问题。

Xcode
ARC 在 Xcode 4.2中有效,当前为beta版,只能用Clang编译(即"Apple LLVM compiler")。有一个设置“Objective-CAutomatic Reference Counting”,设置为YES打开ARC,NO关闭ARC。

如果在老的代码中打开这个设置,将导致大量的错误。ARC不仅仅为你管理内存,它还禁止你手动管理内存。手动调用 retain/release/autorelease 方法在ARC中是被禁止的。由于在非ARC代码中这样的调用随处可见,因此会得到大量的错误。

幸运的是, Xcode 提供了工具对老代码进行自动转换。选择“Edit -> Refactor... -> Convert to Objective-C ARC... ”,Xcode会引导你转换你的代码。尽管有时候也需要你告诉它怎样做,但仍然有许多工作是自动的。

基本功能

Cocoa的内存管理规则很简单:

  1. 如果你alloc、new、copy或者retain一个对象,你必须release或者autorelease它。
  2. 如果你在此之外获得一个对象,但你需要它在内存中存在更长时间,你必须retain 或者copy它。当然,最后你必须release/autorelease它。

这非常适合于自动化。例如你编写了以下代码:

    Foo *foo = [[Foo alloc] init];

    [foo something];

    return;

编译器发觉这段代码的alloc没有匹配的release,于是将代码修改为:

  Foo *foo = [[Foo alloc] init];

   [foo something];

    [foo release];

   return;

 

实际上,编译器插入的不是release消息调用,而是使用运行时函数:

   Foo *foo = [[Foo alloc] init];

   [foo something];

   objc_release(foo);

   return;

这是一种优化。如果release方法未重写,objc_release函数会忽略O-C消息,这样速度会优化一点。

ARC也使代码更安全。大部分程序员根据规则#2,把“长时间”理解为将对象存储到实例变量或类似的地方。这样,就不需要retain和release本地临时对象:

     Foo *foo = [self foo];

     [foo bar];

     [foo baz];

     [foo quux];

但是,这种情况就很危险:

    Foo *foo = [self foo];

     [foo bar];

     [foo baz];

     [self setFoo: newFoo];

     [foo quux]; // crash

解决这个问题的常规方法是在-foo getter方法中return之前使用retain/autorelease。但是这会产生大量的临时对象导致内存使用过多。但是在ARC,将会插入额外语句,类似于这样:

    Foo *foo =objc_retainAutoreleasedReturnValue([self foo]);

     [foo bar];

     [foo baz];

     [self setFoo: newFoo];

     [foo quux]; // fine

     objc_release(foo);

同样,如果你写了一个简单的getter方法,ARC会自动让它更安全:

- (Foo *)foo    

{  

      returnobjc_retainAutoreleaseReturnValue(_foo);    

}

等等,这仍然没有解决临时对象的内存占用问题!ARC最终还是在getter方法中调用retain/autorelease了,而且是retain/autorelease同时调用。这个效率也太低了。

不必担心,就如我前面所说的,ARC会对代码进行优化而忽略这个额外的调用,直接发送消息。为了使reatin/release更快,当二者一起调用时会减少某些操作。

在objc_retainAutoreleaseReturnValue被调用时,它会查看栈并从调用者身上获得返回地址。这样它就能精确地看到在函数结束后还会发生什么。如果编译器优化是打开的,ojbc_retainAutoreleaseReturnValue会使用尾调用优化[1],然会地址将直接指向ojbc_retainAutoreleasedReturnValue调用。

通过检查返回地址,运行时会发现即将有什么冗余的操作。它会取消autorelease,并通过设置标志告诉调用者让它取消它的retain。这样,最终整个代码只进行了一次getter方法中的retain,和一次在调用代码中的release,安全和效率兼顾。

注意,这个优化是完全兼容于非ARC代码的。如果getter方法是非ARC的,标志不会被设置,调用者将执行完整的release/autorelease对。如果getter方法是ARC的而调用者是“非ARC”的,getter方法会看到它要返回的不是特殊的运行时函数代码,因此将执行一个完整的retain/autorelease对。虽然损失了一点效率,但起码不会导致出错。

除此之外,ARC会自动为所有的类创建和填充-dealloc方法,以释放类的是咧变量。你仍然可以手动实现-dealloc方法,对于那些使用了外部资源的类,这是必须的。但对于释放实例变量,这不再是必要的(也是不可能的)。ARC还会为你在最后一句加上[superdealloc],因此连这个步骤你都省去了。在以前,你可能会这样写:

- (void)dealloc    

{  

      [ivar1 release];  

      [ivar2 release];  

      free(buffer);   

      [super dealloc];    

}

而现在,你只要这样写:

- (void)dealloc    

{  

      free(buffer);    

}

在这个-dealloc方法里,仅仅释放了实例变量,不再需要其它操作。

循环引用和弱引用
ARC还是需要程序员自己去解决循环引用(retain cycles),而解决循环引用的最好方法是使用弱引用。

ARC提供了零弱引用,即弱引用的一种,不仅不会导致引用的对象一直存在于内存,而且当引用对象被析构时会自动将其变为nil。零弱引用避免潜在的野指针问题和随之而来的程序崩溃及不可预知的行为。

零弱变量使用前缀 __weak修饰。例如:

@interface Foo : NSObject    

{   

     __weak Bar *_weakBar;    

}

以及本地变量:

__weak Foo *_weakFoo = [object foo];

你可以像让你和其它变量一样使用它,它会在适当的时候自动变成nil:

[_weakBar doSomethingIfStillAlive];

注意,一个零弱变量会在任何时候变成nil。内存管理本身就是一个多线程活动,一个弱引用对象在一个线程中被释放,而在另一个线程确有可能被访问。这样的代码是不行的:

if(_weakBar)   

     [self mustNotBeNil: _weakBar];

应该把弱引用对象存放到一个本地的强引用变量中,例如:

Bar *bar = _weakBar;    

if(bar)   

     [self mustNotBeNil: bar];

现在bar是一个强引用对象,这样就保证了它在整个代码中是一直存活着的(同时不为nil)。

ARC的零弱引用实现需要在OC的引用计数系统和零弱引用系统之间紧密协调。这意味着任何覆盖了retain和release的类都不能被引用为零弱引用。当然这并不多见,某些cocoa类会受到这个限制,比如NSWindow。如果不幸遇到这种情况,你的程序立马崩溃并得到如下消息:

objc[2478]: cannot form weak reference to instance(0x10360f000) of class NSWindow

如果你真的必须对这些类进行弱引用,你可以使用 __unsafe_unretained 代替__weak。这可以创建一个非零弱引用。你必须确保一旦该引用所指向的对象被释放之后,你不会使用该指针(最好手动让它零引用)。小心,非零弱引用就像玩火。可以创建运行在Mac OSX 10.6和iOS 4中的ARC应用程序,但不能使用零弱引用。所有的弱引用都是__unsafe_unretained 的。依我看来,非零弱引用太过危险,这显然降低了ARC在这些系统上的吸引力。

属性
属性和内存管理的联系如此紧密,因此在这里会介绍一些ARC的新特性。

ARC引入了几个新的修饰符。用strong修饰属性表明该属性是一个强引用。将属性修饰为weak则表明是一个零弱引用。unsafe_unretained修饰使用非零弱引用。在使用了@synthesize之后,编译器会创建相同存储类型的实例变量。

已有的修饰符 assign,copy, 和 retain 仍然有效。

注意, assign创建非零弱引用,因此尽可能不要使用。

除了新的修饰符,属性如同以前一样。


块是OC对象,也接受ARC管理。块有一些特殊的内存管理要求,ARC会区别对待。块只可以复制不能retain,也就是说任何时候复制都比retain要好。这是ARC的原则。

另外,如果块作为返回值在当前作用域之后使用,ARC则认为该块必须被复制。而使用非ARC代码,必须在返回语句中显式地调用copy和autorelease:

return [[^{        DoSomethingMagical();     } copy] autorelease];

但是ARC代码则简化为:

return ^{ DoSomethingMagical(); };

但是需要注意:ARC并不会自动复制一个块(转换为id),因此这样写的对的:

dispatch_block_t function(void) { 

       return ^{ DoSomethingMagical();};

}

而这样写是不对的:

id function(void) {  

      return ^{ DoSomethingMagical(); };

}

简单地调用copy消息即可解决这个问题,只需要注意一些地方:

return [^{ DoSomethingMagical(); } copy];

如果你将块作为id参数传递,则需要显式地复制块:

[myArray addObject: [^{ DoSomethingMagical(); }copy]];

庆幸的是,这只是一个小小的bug并不会导致崩溃,可能不久后就会得到修正。

如果你不放心,加一个额外的copy也没什么问题。

ARC还有一个巨大的改变是__block变量。__block修饰符允许块修改块外的变量:

id x;    

__block id y;    

void (^block)(void) = ^{   

    x = [NSString string]; //error  

    y = [NSString string]; //works    

};

在非ARC中,__block有一个副作用,在该变量被块俘获的过程中不会retain目标变量。块会自动retain和release块作用域内的任何对象,但__block指针例外,它相当于是一个弱指针。这是一种常用的方案,用__block避免循环引用。

在ARC中,__block会retain住目标对象,就如同块内的局部变量。使用__block将不可能避免循环引用。因此,可用__weak来代替。

Toll-FreeBridging免费桥接
ARC只对OC类型有效。CoreFoundation类型必须由程序员手动管理。因此,为避免歧义,ARC禁止在指针和OC对象及其他指针类型,包括CoreFoundation对象指针之间进行转换。下列代码,在手动内存管理中是普遍存在的,但在ARC中是被禁止的:

id obj = (id)CFDictionaryGetValue(cfDict, key);

为了让编译通过,你必须使用特殊的转换修饰词告诉ARC相关的所有权语义。这些修饰词包括:

 __bridge, __bridge_retained, 以及 __bridge_transfer。

最容易理解的是__bridge。这是直接转换,不考虑最终的关系。ARC接收这个值然后进行正常的管理。要达到前面的目的可以这样写:

id obj = (__bridge id)CFDictionaryGetValue(cfDict,key);

其他的转换修饰词会传递所有权,将所有权从ARC转移出来或者将所有权转移给ARC。这将有利于简化桥接的代码。

举个例子,在某种情况下,返回对象需要被释放。

NSString *value = (NSString *)CFPreferencesCopyAppValue(CFSTR("someKey"),CFSTR("com.company.someapp"));

[self useValue: value];    

[value release];

如果使用ARC,则需要加上__bridge,同时将release去掉,其他内容不变。这样会导致一个内存泄露:

NSString *value = (__bridge NSString*)CFPreferencesCopyAppValue(CFSTR("someKey"),CFSTR("com.company.someapp"));

[self useValue: value];

代码中,Copy的使用必须用release来平衡。在初始化value时,ARC产生了一个retain,当value不再使用时,又用一个release来平衡retain。因此,Copy未被平衡,这个对象产生了泄露。

我们可以这样子解决:

CFStringRef valueCF =CFPreferencesCopyAppValue(CFSTR("someKey"),CFSTR("com.company.someapp"));   

NSString *value = (__bridge NSString *)valueCF;    

CFRelease(valueCF);     

[self useValue: value];

但这就变得比较啰嗦了。由于免费桥接的出发点是尽可能的无缝,而ARC的出发点是减少内存管理代码,最好能将代码变得更简单直白。

__bridge_transer修饰词用于解决这个问题。相对于简单地将指针交由ARC管理,__bridge_transfer还会转移所有权。使用__bridge_transfer时,它会告诉ARC,这个对象已经retain,ARC不需要再retain。因为ARC拥有了所有权,就可以在不再需要对象时release它。最终结果应该是这样:

NSString *value = (__bridge_transfer NSString*)CFPreferencesCopyAppValue(CFSTR("someKey"),CFSTR("com.company.someapp"));    

[self useValue: value];

免费桥接以两种方式都能工作。如前面所说,ARC禁止从一个OC对象转换为CoreFoundaiton对象。这段代码在ARC中无法编译通过:

CFStringRef value = (CFStringRef)[selfsomeString];    

UseCFStringValue(value);

如果使用一个__bridge,代码可以编译,但这样的代码是危险的:

CFStringRef value = (__bridge CFStringRef)[selfsomeString];   

UseCFStringValue(value);

因为ARC不会管理value的生命周期了,它将立即放弃对象的所有权。在对象被传递给UseCFStringValue之前,有可能导致程序崩溃或不可预知的行为。通过__bridge_retained,我们可以告诉ARC将所有权转移到我们手里。由于所有权被传递,现在我们负责对象的释放。就像任何CF代码一样:

CFStringRef value = (__bridge_retainedCFStringRef)[self someString];

UseCFStringValue(value);    

CFRelease(value);

类型转换修饰词在免费桥接之外也是有用的。当你想将一个对象指针存储到未托管的非OC对象时,它们都能帮上忙。在Cocoa中存在各种各样的void* context指针,典型的如sheets这个例子。在非ARC代码:

NSDictionary *contextDict = [NSDictionarydictionary...];    

[NSApp beginSheet: sheetWindow

        modalForWindow:mainWindow

         modalDelegate:self

        didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)  

        contextInfo:[contextDict retain]];      

 

- (void)sheetDidEnd: (NSWindow *)sheet returnCode:(NSInteger)code contextInfo: (void *)contextInfo    

{    

   NSDictionary *contextDict = [(id)contextInfo autorelease]; 

   if(code == NSRunStoppedResponse)            ...    

}

跟上面一样,在ARC下面不能编译。因为ARC不允许在对象和非对象指针之间进行直接转换。但是使用转换修饰词,ARC是允许转换的,但同时要让ARC为我们进行必要的内存管理:

NSDictionary *contextDict = [NSDictionarydictionary...];    

[NSApp beginSheet: sheetWindow

        modalForWindow: mainWindow

         modalDelegate:self

        didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)          contextInfo: (__bridge_retained void *)contextDict];      

- (void)sheetDidEnd: (NSWindow *)sheet returnCode:(NSInteger)code contextInfo: (void *)contextInfo    

{   

     NSDictionary *contextDict =(__bridge_transfer NSDictionary *)contextInfo;   

     if(code == NSRunStoppedResponse)       

     ...    

}

归纳起来:

  • __bridge 可以简单地在ARC和非ARC之间传递指针,但不传递所有权。
  • __bridge_transfer 可以将非OC指针传递给OC指针,同时传递所有权,以便ARC为你释放它。
  • __bridge_retained 将一个OC指针传递给非OC指针,同时传递所有权,由你,程序员,负责此后进行CFRelease,或者释放对象的所有权。

结构
在ARC中,结构和OC对象指针很难混淆。问题是编译器很难知道一个结构何时被copy和destroy,也找不到地方去插入retain和release。而且将对象指针放到结构中也不是很常见的行为,ARC完全放弃了这一块。如果你想将OC对象指针放到结构中,必须用__unsafe_unretained进行修饰,并解决所有因此产生的问题。

因为通常不会把OC指针放入结构中,你很可能不会遇到那些问题。否则,最好将结构改变为轻量级的OC类。这样,由ARC为你管理内存,问题也就不复存在了。

文档及资源
由于苹果官方的ARC文档仍然未公开以及Xcode4.2仍然是beta版,你可以从Clang的网站获得大量信息:

http://clang.llvm.org/docs/AutomaticReferenceCounting.html

结论
ARC减轻了程序员的内存管理压力。ARC不是垃圾回收。它无法检查出循环引用,必须由程序员自行处理并打断循环引用。编写Cocoa代码仍然需要大量烦人的工作,但零弱引用是解决循环引用的有力工具。

CoreFoundation对象和免费桥接要更麻烦。ARC仅能处理OC,程序员仍然需要手动管理CoreFoundation。当在OC指针和CoreFoundation指针之间转换时,需要使用某种__bridge转换修饰词,用于通知ARC转换时的内存管理动作。这就是今天所讨论的苹果最新的编程语言技术。

 


[1] 尾调用优化:一种避免为函数分配栈内存的做法,让调用函数简单地返回它所调用的函数返回值。看几个例子:

function foo1(data)

return A(data) + 1;

 

function foo2(data)

return A(data);

只有foo2属于尾调用。foo1在调用A之后还要进行一次+1,这样就需要将控制返回给foo1并为foo1建栈,无法直接把A的栈桢直接替换foo1的栈桢。因此foo1可以修改为:

function foo1(data)

return A(data,1);

让A对data计算之后,在返回结果之前加1。这样就是尾调用优化,将最后一句替换为对A的调用。由于对A的调用处于函数的最后,因此foo1函数之前的各种状态已经不影响最终计算结果,我们完全可以把本次函数栈中的数据丢弃,把空间让给最后的尾调用。这样的优化便使得函数调用减少了一次建栈操作(即额外的栈空间分配)。如果尾调用最后调用的是自己,则成为尾递归,优化的意义就更加明显,因为这意味着即使是“无限”递归也不会出现堆栈溢出。这便是尾调用优化的优势。

 

你可能感兴趣的:(ARC 问答)