ObjectiOS开发iOS Developer

【译】苹果官方手册:开始使用ARC

2016-08-11  本文已影响365人  hlwz5735

自动引用计数(ARC)是一项编译器功能,可以给Objective-C提供自动内存管理的能力。ARC使得程序员能专注于应用程序的代码、对象图和对象间关系上,而不是考虑那些保持(retain)和释放(release)操作。


手动及自动引用计数的区别手动及自动引用计数的区别

概览

ARC的工作原理是在编译期间增加相关代码,从而确保对象们只在它们所需的时段内存活,而不是永远存在。从概念上来说,它和手动引用计数(参考高级内存管理编程手册译文))遵循相同的内存管理机制,只不过它会自动添加适当的内存管理调用代码。
为了使得编译器能添加正确的代码,ARC限制了某些方法的调用,以及你使用对象桥接(toll-free bridging,参考对象桥接类别)的方式。同时ARC也为对象的引用及属性声明(属性声明可以方便地为该类成员声明访问方法,并且可以默认地实现它们)引入了新的存活时间修饰符。
OS X v10.6 或 OS X v10.7(64位应用程序)上的Xcode 4.2开始支持ARC,iOS 4 和 iOS 5或更高版本支持ARC。但OS X v10.6 和 iOS 4 上的ARC不支持弱引用。
Xcode提供了自动转换为ARC的工具(如移除retainrelease的调用)并帮助你自动修复迁移无法解决的问题(使用Edit > Refactor > Convert to Objective-C ARC)的工具。这个转换工具将会把工程内所有的文件转化为使用ARC的模式。如果在某些文件中使用手动引用计数更加方便的话,你也可以选择仅在部分文件中使用ARC。
参考:

ARC 概述

ARC会分析对象的生存时间并自动地在编译期间插入调用适当的内存管理方法的代码,从而取代了以往不得不记住何时要使用retainreleaseautorelease的那些日子。编译器同样也会产生适当的dealloc方法。总的来说,如果你只使用了ARC的话,那么传统的Cocoa命名约定就只在你需要跟使用手动引用计数的代码交互的时候才显得重要。
一个完整并正确的Person类的实现看起来像这样:

@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@property NSNumber *yearOfBirth;
@property Person *spouse;
@end
 
@implementation Person
@end

(默认情况下,对象的属性为strongstrong的具体介绍参考下文ARC引入的新的存活时间修饰符。)
使用了ARC之后,可以这样来实现一个contrived方法:

- (void)contrived {
    Person *aPerson = [[Person alloc] init];
    [aPerson setFirstName:@"William"];
    [aPerson setLastName:@"Dudney"];
    [aPerson setYearOfBirth:[[NSNumber alloc] initWithInteger:2011]];
    NSLog(@"aPerson: %@", aPerson);
}

ARC会处理内存管理的问题,所以不论Person还是NSNumber对象都不会发生泄漏。也可以这样安全地实现Person类的takeLastNameFrom:方法:

- (void)takeLastNameFrom:(Person *)person {
    NSString *oldLastname = [self lastName];
    [self setLastName:[person lastName]];
    NSLog(@"Lastname changed from %@ to %@", oldLastname, [self lastName]);
}

ARC可以确保在NSLog之前oldLastName不被释放。

ARC带来的新规则

ARC引入了一些其他编译模式不存在的新规则。这些规则的意图是提供一个全面可信任的内存管理模型;有时候,它们直接地带来了最好的实践体验,也有时候它们简化了代码,甚至在你丝毫没有关注内存管理问题的时候帮你解决了问题。如果违反了这些规则,你将会得到一个即使编译器错误,而不是可能在运行期间才会发生的小bug。

为了和手动保持-释放的代码交互,ARC对方法的命名提出了限制:

// 不可行:
@property NSString *newTitle;
// 可行:
@property (getter=theNewTitle) NSString *newTitle;

ARC引入的新的存活时间修饰符

ARC为对象引入了一些新的存活时间修饰符以及弱引用功能。弱引用并不会扩展它指向的对象的存活时间,并且当该对象没有被强引用的时候自动置为nil

建议使用这些修饰符来管理程序的对象图。特别地,ARC不会防止强引用循环(之前叫做保持循环——参考实际内存管理译文))。审慎地使用弱引用将会帮助确保没有出现强引用循环。

属性(Property)的特征词

(此处的“修饰词”原文为“attribute”,一般应该翻译成“属性”,但因为“Property”一般翻译过来也是“属性”,所以为避免混淆,将其翻译为“特征词”)

关键字weakstrong被作为新的属性声明特征词被引入。示例如下:

// 下边的声明方式和 “@property(retain) MyClass *myObject;” 等价
@property(strong) MyClass *myObject;
 
// 下边的声明和“@property(assign) MyClass *myObject;”类似
// 不同之处是当MyClass实例被销毁时,
// 这个属性值将会被置为nil,而不是变成野指针
@property(weak) MyClass *myObject;

在使用ARC的项目里,strong是默认的对象类型。

变量修饰词

有如下变量修饰词:

__strong
__weak
__unsafe_unretained
__autoreleasing

我们需要正确地修饰变量。当在声明变量的时候使用修饰词时,正确的格式如下:

类名 * 修饰词 变量名;

例如:

MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;

其他的变体在技术上来说是不正确的,但会被编译器“忽略”。详情可以参考 http://cdecl.org/
在栈中需要小心使用__weak变量。考虑如下代码:

NSString * __weak string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];
NSLog(@"string: %@", string);

尽管string在首条命令后仍被使用,但在之后并没有其它强引用指向这个字符串对象;所以它就立即被销毁了。在打印语句中显示,string的值为空。(编译器在这种情况下会发出警告。)
同时也要关注由引用传递的对象。下边的代码可以运行:

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
    // 报告这个错误。
    // ...

然而,异常的定义是隐含的:

NSError * __strong e; ```
并且方法的定义将通常是:

-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error; ```
于是编译器会将代码重写为这个样子:

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
    // 报告这个错误
    // ...

由于局部变量声明(__strong)和参数声明(__autoreleasing)之间的不匹配,导致编译器会创建临时变量。当需要获取一个__strong变量的地址时,可以通过给参数声明id __strong *来获取到原本的指针。不然的话,就将变量声明为__autoreleasing

使用存活时间修饰符来避免强引用循环

例如,你的程序具有这样一种结构,对象与对象之间构成一种双亲-子女的层级关系,并且双亲需要依赖子女值得改变做出变化,这时候可以令双亲对子女为强关系,而子女对双亲为弱关系。其它的情况可能会更加微妙,特别是当它们调用了块对象(块对象是一种C级别的句法和运行时功能,它能用来组成那些可作为参数传递的、随意存储的并可以用在多线程中的函数表达式。)的时候。
在手动引用计数模式下,__block id x;不会保持x。但在ARC模式中,__block id x;默认会保持x(就像其他的值一样)。为了在ARC模式中得到和手动引用计数一样的行为,可以使用__unsafe_unretained __block id x;。然而,拥有一个没有保持的变量是危险的(因为它可能会变成野指针),所以并不推荐这种方式。两种更好的选择是使用__weak(如果不需要支持iOS 4或OS X v10.6),或将__block的值设为nil来打破保持循环。
下边展示了在手动引用计数时常常会出问题的代码片段:

MyViewController *myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
   [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];

就像我们描述的那样,作为代替,可以使用__block修饰符,并在completion处理语句内将myController置为nil

MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};

或者,可以使用一个临时的__weak变量。下边的例子是其实现:

MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler =  ^(NSInteger result) {
    [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};

然而对于间接循环来说,则需要这样做:

MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // 可能什么都没有……
    }
};

在一些情况下如果这个类不能很好地配合__weak使用的话,可以尝试用__unsafe_unretained。然而,这在间接循环中不太现实,因为验证一个__unsafe_unretained指针是不是依然有效很难,甚至是不可能的。

ARC使用新的管理自动释放池的声明方式

使用ARC时,不能直接使用NSAutoreleasePool类来管理自动释放池。而是应该使用@autorelease代码块:

@autoreleasepool {
    // 代码,例如创建大量临时对象的循环。
}

这种简洁的结构使得编译器可以推断出引用计数的状态。在进入代码块时,一个自动释放池进栈。在正常退出(break,return,goto,fall-through等语句)时,自动释放池被弹出。为了和已存在的代码共存,如果因为异常造成的退出,自动释放池不会被弹出。
这种句法在所有的Objective-C模式中都是可用的。这比使用NSAutoreleasePool更有效率;所以我们鼓励使用这种方式替换NSAutoreleasePool

管理出口的语句在多个平台实现统一

在iOS和OS X中用来声明出口(outlet,出口本质是一个属性,但它的值可以在nib文件中图形化地设置)的语句因为ARC而发生了改变,并开始在两个平台统一起来。出口应该是weak的,但那些在nib文件(或故事板场景)的来自文件所有者的顶级对象,那些对象应当是strong的。
完整的详细信息参考资源编程指南中的Nib 文件

栈变量被初始化为nil

使用ARC之后,强、弱和自动释放的栈变量现在会默认初始化为nil。例如:

- (void)myMethod {
    NSString *name;
    NSLog(@"name: %@", name);
}

日志会输出空的name值,而不是崩溃。

使用编译器标识来启用和禁用ARC

可以使用一个新标识-fobjc-arc来启用ARC。如果在某些文件中使用手动引用计数更方便的话,也可以仅对部分文件使用ARC。对于默认即是ARC模式的工程,可以使用另一个新的编译器标识-fno-objc-arc来禁用针对该文件的ARC。
OS X v10.6 或 OS X v10.7(64位应用程序)上的Xcode 4.2开始支持ARC,iOS 4 和 iOS 5或更高版本支持ARC。但OS X v10.6 和 iOS 4 上的ARC不支持弱引用。Xcode 4.1及更早版本不支持ARC。

管理对象桥接

在很多Cocoa应用程序中需要使用Core Foundation库风格的对象。包括出自Core Foundation框架本身(例如CFArrayRefCFMutableDictionaryRef)的还有出自符合Core Foundation约定标准的其他框架(你可能会用到CGColorSpaceRefCGGradientRef这些类型)的对象。
编译器会自动管理Core Foundation对象的存活时间,必须根据Core Foundation内存管理规则(参考Core Foundation的内存管理编程指南)调用CFRetainCFRelease
如果要在Objective-C及Core Foundation风格对象之间做类型转换,需要使用类型转换(在objc/runtime.h中定义)及Core Foundation风格宏(在NSObject.h中定义)告诉编译器关于对象间所有权关系的语义:

-(void)logFirstNameOfPerson:(ABRecordRef)person {
 
    NSString *name = (NSString *)ABRecordCopyValue(person, kABPersonFirstNameProperty);
    NSLog(@"Person's first name: %@", name);
    [name release];
}

可以将其改写为:

-(void)logFirstNameOfPerson:(ABRecordRef)person {
 
    NSString *name = (NSString *)CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
    NSLog(@"Person's first name: %@", name);
}

编译器处理Cocoa方法中返回的CF对象

编译器可以理解Objective-C方法返回的遵循Cocoa命名约定(参考高级内存管理编程手册译文))的Core Foundation类型。例如,编译器知道,在iOS中,由UIColorCGColor方法返回的CGColor对象,并没有被拥有。仍然必须使用适当的类型转换,就像下边的例子:

NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
[colors addObject:(id)[[UIColor lightGrayColor] CGColor]];

使用所有权关键字转换函数参数

当在函数调用时在Objective-C和Core Foundation对象之间转换时,需要告诉编译器有关传入对象的所有权信息。Core Foundation对象的所有权规则在Core Foundation内存管理规则中给出(参考 Core Foundation的内存管理编程指南);Objective-C对象的规则在高级内存管理编程手册译文))中给出。
下面的代码片段,传入CGGradientCreateWithColors的数组需要适当的转换。由arrayWithObjects:返回的对象所有权并不需要传入这个函数,所以这个转换用了__bridge

NSArray *colors = <#An array of colors#>;
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);

下面代码片段展示了一个方法的实现。要记住使用Core Foundation内存管理规则中提及的Core Foundation内存管理函数。

- (void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
    CGFloat locations[2] = {0.0, 1.0};

    NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
    [colors addObject:(id)[[UIColor lightGrayColor] CGColor]];
    
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
    CGColorSpaceRelease(colorSpace);  // 释放拥有的Core Foundation 对象
    CGPoint startPoint = CGPointMake(0.0, 0.0);
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));

    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint,
        kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);

    CGGradientRelease(gradient);  // 释放拥有的Core Foundation 对象。
}

转换工程时的常见问题

在迁移一个已经存在的项目时,很可能会遇到许多问题。这里列出常见问题的解决方案。

while ([x retainCount]) { [x release]; }
[super init];

简易的修复方式是:

self = [super init];

更加合理的修复方式在继续之前再检查结果是否为nil

self = [super init];
if (self) {
    ...
@interface MyClass : Superclass {
    id thing; // 弱引用。
}
// ...
@end
// 
@implementation MyClass
-(id)thing {
    return thing;
}
-(void)setThing:(id)newThing {
    thing = newThing;
}
// ...
@end

在ARC中,实例变量默认使用强引用——将一个对象分配给一个实例变量会直接扩展对象的存活时间。转换工具无法确定何时实例变量需要设为弱引用。为了维持和之前一样的行为,必须指明实例变量是弱引用,或使用属性声明。

@interface MyClass : Superclass {
    id __weak thing;
}
// ...
@end
 //
@implementation MyClass
-(id)thing {
    return thing;
}
-(void)setThing:(id)newThing {
    thing = newThing;
}
// ...
@end

或:

@interface MyClass : Superclass
@property (weak) id thing;
// ...
@end
//
@implementation MyClass
@synthesize thing;
// ...
@end
struct X { id x; float y; };

这是由于x默认是强引用,编译器无法保证令所有的相关的代码正常地运行。例如,如果通过某些代码给这些结构体传入了一个指针,并在之后进行了free操作,所有id指向的对象都必须在结构体释放之前被释放掉。编译器并不能可靠地做到这一点,所以强引用的id是不能再ARC模式下的结构体中存在的。
这里有一些可行的解决方案:
1.使用Objective-C对象代替结构体。
我们认为这是最佳的解决方案。
2.如果使用Objective-C对象是次要选项的话,(可能你需要这种结构体的密集数组)考虑使用void *代替。
这需要使用明确类型转化,接下来会讨论。
3.将对象的引用类型标记为__unsafe_unretained
这种方法可能对如下的半公共代码比较有用:

struct x { NSString *S;  int X; } StaticArray[] = {
    @"foo", 42,
    @"bar, 97,
    ...
};

可以这样声明这个结构体:

struct x { NSString * __unsafe_unretained S; int X; }

这可能会造成一些困难,并且如果对象在这之外被释放的话这个指针就是不安全的,但它确实对诸如字符串常量之类的从一开始就确定永久存活的对象非常有用。

常见问答

我该怎么理解ARC?它在哪儿添加了ratain/release?

尝试不要去琢磨retain/release在那儿被放置和调用这回事,而是多思考应用程序的逻辑与算法。多琢磨对象的“强和弱”、对象间的关系以及保持循环的避免。

我需要给对象写dealloc方法吗?

也许是吧。
因为ARC并不会自动malloc/free,所以对Core Foundation对象的生存时间管理,文件描述符等,这类资源仍需要通过编写dealloc方法来释放。
不应(事实上也不能)释放实例变量,但你可能会给系统类和其他非ARC生成的代码调用[self setDelegate:nil]。
ARC中的
dealloc方法不需要——或者说不允许——调用[super dealloc];**对超类的调用链会在运行时自动处理。

在ARC中仍然存在保持循环的问题吗?

是的。
ARC自动地保持/释放,所以也继承了保持循环的问题。幸运的是,将代码转移至ARC后将很少发生内存泄露,因为属性之间的关系已经明确了。

在ARC中代码块如何工作?

在ARC模式下,代码块“只在”你将其传入栈时工作,例如在返回语句内。不再需要调用代码块的复制了。
要注意的一件事是,NSString * __block myString在ARC模式下被保持了,这不会造成野指针的问题。如果要执行之前的行为,使用__block NSString * __unsafe_unretained myString或(仍然是更好的选择)__block NSString * __weak myString

我可不可以在Snow Leopard上开发基于ARC的OS X应用程序?

不可以。Snow Leopard版的Xcode 4.2完全不支持OS X上的ARC,因为它并没有包含10.7 的SDK。但Snow Leopard版Xcode 4.2支持iOS上的ARC。Lion版的Xcode 4.2同时支持OS X和iOS。这意味着你需要Lion系统来构建运行在Snow Leopard上的ARC应用程序。

我可以创建一个包含被已保持了的指针的C数组吗?

可以,举例如下:

// 使用calloc()来获取填满了0的内存区域。
__strong SomeClass **dynamicArray = (__strong SomeClass **)calloc(entries, sizeof(SomeClass *));
for (int i = 0; i < entries; i++) {
     dynamicArray[i] = [[SomeClass alloc] init];
}
 
// 当完成工作后,将所有成员设为nil,从而告诉ARC释放对象。
for (int i = 0; i < entries; i++) {
     dynamicArray[i] = nil;
}
free(dynamicArray);

一些需要记住的要点如下:

ARC会拖慢运行速度吗?

这取决于你的标准,但通常来说是“不”。编译器可以高效地排除一些无用的retain/release调用,并且很多的经历都被投入到提升Objective-C的运行速度上去了。特别地,当方法的调用者是ARC代码时,常见的“返回一个保持/自动释放对象”代码段比没有把对象放进自动释放池的情况快很多。
需要注意的一个问题是优化程序不会在默认调试结构中使用,所以预计在-O0模式
下将会比-Os模式下看到更多的retain/release调用。

ARC可以运行在ObjC++模式下吗?

当然可以。你甚至还可以在类或容器中放置强/弱的id对象。ARC编译器会在复制构造函数及析构函数等方法中生成retain/release逻辑,从而使之运行。

哪些类不支持弱引用?

当前不能给实例创建弱引用的类如下:

NSATSTypesetter, NSColorSpace, NSFont, NSMenuView, NSParagraphStyle, NSSimpleHorizontalTypesetter, NSTextView
补充:在OS X v10.7 中,不能为这几个类创建弱引用:NSFontManager, NSFontPanel, NSImage, NSTableCellView, NSViewController, NSWindow,NSWindowController。此外,在OS X v10.7 中,AV Foundation框架中没有任何类支持弱引用。

对于属性声明来说,你应该使用assign而不是weak;对变量来说,你应该使用__unsafe_unretained代替__weak
此外,在ARC中不能给NSHashTableNSMapTableNSPointerArray的实例创建弱引用。

当我继承一个使用了NSCopyObject的类,如NSCell时,我需要做些什么?

没有任何特殊的。ARC会关注之前需要你明确添加额外的ratain语句的地方。有了ARC,所有的复制方法只需要复制实例变量就可以了。

我能只指定一部分文件使用ARC吗?

可以。
当你将一个工程迁移到ARC模式下时,-fobjc-arc编译器标识默认会给所有Objective-C源文件设置上。你可以使用-fno-objc-arc编译器标识给某个具体类禁用ARC功能。在Xcode中Build Phases选项卡里,打开Compile Sources组展开源文件列表。双击你想设置标识的文件,在弹出的面板里输入-fno-objc-arc并点击Done,完成设置。

Xcode设置界面Xcode设置界面

在Mac上,GC(垃圾回收)功能过时了吗?

垃圾回收功能自从OS X Mountain Lion v10.8版本就过时了,并将在OS X的未来版本中移除。推荐使用自动引用计数来代替这项技术。着力于迁移现有的应用程序,Xcode 4.3及更新版本中的ARC迁移工具支持将基于垃圾回收的OS X应用程序迁移至ARC。

要点:那些想要上架Mac APP Store的应用,苹果强烈建议尽快将垃圾回收替换为ARC,因为Mac App Store的方针(参考Mac App Store审核方针)禁止使用过时的技术。

上一篇 下一篇

猜你喜欢

热点阅读