20 / 12 / 25

「重学 OC」内存管理11 - Objective-C中的ARC

ARC全称Automatic Reference Counting,自动引用计数内存管理,是苹果在 iOS 5、OS X Lion 引入的新的内存管理技术。ARC是一种编译器功能,它通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retain、release和autorelease代码来自动管理对象的内存,省去了在MRC手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量,让开发者可以专注于应用程序的代码、对象图以及对象间的关系上。

下图是苹果官方文档给出的从MRC到ARC的转变。

ARC的工作原理是在编译时添加相关代码,以确保对象能够在必要时存活,但不会一直存活。从概念上讲,它通过为你添加适当的内存管理方法调用来遵循与MRC相同的内存管理规则。

为了让编译器生成正确的代码,ARC限制了一些方法的使用以及你使用桥接(toll-free bridging)的方式,ARC还为对象引用和属性声明引入了新的生命周期修饰符。

ARC在Xcode 4.2 for OS X v10.6 and v10.7 (64-bit applications) 以及iOS 4 and iOS 5应用程序中提供支持。但OS X v10.6 and iOS 4不支持weak弱引用。

Xcode 提供了一个迁移工具,可以自动将MRC代码转换为ARC代码(如删除retain和release调用),而不用重新再创建一个项目(选择 Edit > Convert > To Objective-C ARC)。迁移工具会将项目中的所有文件转换为使用ARC的模式。如果对于某些文件使用MRC更方便的话,你可以选择仅在部分文件中使用ARC。

ARC 概述

ARC会分析对象的生存期需求,并在编译时自动插入适当的内存管理方法调用的代码,而不需要你记住何时使用retain、release、autorelease方法。编译器还会为你生成合适的dealloc方法。一般来说,如果你使用ARC,那么只有在需要与使用MRC的代码进行交互操作时,传统的 Cocoa 命名约定才显得重要。 Person 类的完整且正确的实现可能如下所示:

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

默认情况下,对象属性是strong。

使用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引入了一些在使用其他编译器模式时不存在的新规则。这些规则旨在提供完全可靠的内存管理模型。有时候,它们直接地带来了最好的实践体验,也有时候它们简化了代码,甚至在你丝毫没有关注内存管理问题的时候帮你解决了问题。在ARC下必须遵守以下规则,如果违反这些规则,就会编译错误。

不能使用 retain / release / retainCount / autorelease

在ARC下,禁止开发者手动调用这些方法,也禁止使用@selector(retain),@selector(release) 等,否则编译不通过。但你仍然可以对 Core Foundation 对象使用CFRetain、CFRelease等相关函数。

不能使用 NSAllocateObject / NSDeallocateObject

在ARC下,禁止开发者手动调用这些函数,否则编译不通过。 你可以使用alloc创建对象,而Runtime会负责dealloc对象。

须遵守内存管理的方法命名规则

在MRC下,通过 alloc / new / copy / mutableCopy 方法创建对象会直接持有对象,我们定义一个 “创建并持有对象” 的方法也必须以 alloc / new / copy / mutableCopy 开头命名,并且必须返回给调用方所应当持有的对象。如果在ARC下需要与使用MRC的代码进行交互,则也应该遵守这些规则。
为了允许与MRC代码进行交互操作,ARC对方法命名施加了约束:访问器方法的方法名不能以new开头。这意味着你不能声明一个名称以new开头的属性,除非你指定一个不同的`getterName:`
```
// Won't work:
@property NSString *newTitle;

// Works:
@property (getter = theNewTitle) NSString *newTitle;
```

不能显式调用 dealloc

无论在MRC还是ARC下,当对象引用计数为 0,系统就会自动调用dealloc方法。大多数情况下,我们会在dealloc方法中移除通知或观察者对象等。
在MRC下,我们可以手动调用dealloc。但在ARC下,这是禁止的,否则编译不通过。
在MRC下,我们实现dealloc,必须在实现末尾调用`[super dealloc]`。
```
// MRC
- (void)dealloc
{
    // 其他处理
    [super dealloc];
}
```
而在ARC下,ARC会自动对此处理,因此我们不必也禁止写[super dealloc],否则编译错误。
```
// ARC
- (void)dealloc
{
    // 其他处理
    [super dealloc]; // 编译错误:ARC forbids explicit message send of 'dealloc'
}
```

使用 @autoreleasepool 块替代 NSAutoreleasePool

在ARC下,自动释放池应使用@autoreleasepool,禁止使用NSAutoreleasePool,否则编译错误。
```
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
// error:'NSAutoreleasePool' is unavailable: not available in automatic reference counting mode
```

不能使用区域(NSZone)

对于现在的运行时系统(编译器宏 __ OBJC2 __ 被设定的环境),不管是MRC还是ARC下,区域(NSZone)都已单纯地被忽略。
> NSZone: 摘自《Objective-C 高级编程:iOS 与 OS X 多线程和内存管理》

NSZone 是为防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。 但是,现在的运行时系统已经忽略了区域的概念。运行时系统中的内存管理本身已极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。 下图是使用多重区域防止内存碎片化的例子:

对象型变量不能作为 C 语言结构体(struct / union)的成员

C 语言的结构体(struct / union)成员中,如果存在 Objective-C 对象型变量,便会引起编译错误。
> 备注: Xcode10 开始支持在 ARC 模式下在 C Struct 里面引用 Objective-C 对象。之前可以用 Objective-C++。
```
struct Data {
    NSMutableArray *mArray;
};
// error:ARC forbids Objective-C objs in struct or unions NSMutableArray *mArray;
```
虽然是 LLVM 编译器 3.0,但不论怎样,C 语言的规约上没有方法来管理结构体成员的生存周期。因为ARC把内存管理的工作分配给编译器,所以编译器必须能够知道并管理对象的生存周期。例如 C 语言的自动变量(局部变量)可使用该变量的作用域管理对象。但是对于 C 语言的结构体成员来说,这在标准上就是不可实现的。因此,必须要在结构体释放之前将结构体中的对象类型的成员释放掉,但是编译器并不能可靠地做到这一点,所以对象型变量不能作为 C 语言结构体的成员。
这个问题有以下三种解决方案:
1. 使用 Objective-C 对象替代结构体。这是最好的解决方案。
2. 将 Objective-C 对象通过Toll-Free Bridging强制转换为void *类型。
3. 对 Objective-C 对象附加__unsafe_unretained修饰符。
    ```
    struct Data {
        NSMutableArray __unsafe_unretained *mArray;
    };
    ```
    附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便有可能遭遇内存泄漏或者程序崩溃。这点在使用时应多加注意。
    ```
    struct x { NSString * __unsafe_unretained S; int X; }
    ```
    __unsafe_unretained指针在对象被销毁后是不安全的,但它对诸如字符串常量之类的从一开始就确定永久存活的对象非常有用。

显式转换 “id” 和 “void *” —— 桥接

在MRC下,我们可以直接在 id 和 void * 变量之间进行强制转换。
```
id obj = [[NSObject alloc] init];
void *p = obj;
id o = p;
[o release];
```
但在ARC下,这样会引起编译报错:在Objective-C指针类型id和C指针类型void *之间进行转换需要使用Toll-Free Bridging。
```
id obj = [[NSObject alloc] init];
void *p = obj; // error:Implicit conversion of Objective-C pointer type 'id' to C pointer type 'void *' requires a bridged cast
id o = p;      // error:Implicit conversion of C pointer type 'void *' to Objective-C pointer type 'id' requires a bridged cast
[o release];   // error:'release' is unavailable: not available in automatic reference counting mode
```

所有权修饰符

ARC为对象引入了几个新的生命周期修饰符(我们称为 “所有权修饰符”)以及弱引用功能。弱引用weak不会延长它指向的对象的生命周期,并且该对象没有强引用(即dealloc)时自动置为nil。 你应该利用这些修饰符来管理程序中的对象图。特别是,ARC不能防止强引用循环(以前称为Retain Cycles。明智地使用弱引用weak将有助于确保你不会创建循环引用。

属性关键字

ARC中引入了新的属性关键字strong和weak,如下所示:

// 以下声明同:@property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;
 
// 以下声明类似于:@property(assign)MyClass *myObject;
// 不同的是,如果 MyClass 实例被释放,属性值赋值为 nil,而不像 assign 一样产生悬垂指针。
@property(weak) MyClass *myObject;

strong和weak属性关键字分别对应__strong和__weak所有权修饰符。在ARC下,strong是对象类型的属性的默认关键字。

在ARC中,对象类型的变量都附有所有权修饰符,总共有以下 4 种。

__strong // 是默认修饰符。只要有强指针指向对象,对象就会保持存活。
__weak // 指定一个不使引用对象保持存活的引用。当一个对象没有强引用时,弱引用weak会自动置为nil。
__unsafe_unretained // 指定一个不使引用对象保持存活的引用,当一个对象没有强引用时,它不会置为nil。如果它引用的对象被销毁,就会产生悬垂指针。
__autoreleasing // 用于表示通过引用(id *)传入,并在返回时(autorelease)自动释放的参数。

在对象变量的声明中使用所有权修饰符时,正确的格式为:

ClassName * qualifier variableName;

例如:

MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;

其它格式在技术上是不正确的,但编译器会 “原谅”。也就是说,以上才是标准写法。

__strong

__strong修饰符为强引用,会持有对象,使其引用计数 +1。该修饰符是对象类型变量的默认修饰符。如果我们没有明确指定对象类型变量的所有权修饰符,其默认就为__strong修饰符。

id obj = [NSObject alloc] init];
// -> id __strong obj = [NSObject alloc] init];

__weak

如果单单靠__strong完成内存管理,那必然会发生循环引用的情况造成内存泄漏,这时候__weak就出来解决问题了。 __weak修饰符为弱引用,不会持有对象,对象的引用计数不会增加。__weak可以用来防止循环引用。 以下单纯地使用__weak修饰符修饰变量,编译器会给出警告,因为NSObject的实例创建出来没有强引用,就会立即释放。

id __weak weakObj = [[NSObject alloc] init]; // ⚠️Assigning retained object to weak variable; object will be released after assignment
NSLog(@"%@", obj);
//  (null)

以下NSObject的实例已有强引用,再赋值给__weak修饰的变量就不会有警告了。

id __strong strongObj = [[NSObject alloc] init];
id __weak weakObj = strongObj;

当对象被dealloc时,指向该对象的__weak变量会被赋值为nil。

备注:__weak仅在ARC中才能使用,在MRC中是使用__unsafe_unretained修饰符来代替。

__unsafe_unretained

__unsafe_unretained修饰符的特点正如其名所示,不安全且不会持有对象。

注意: 尽管ARC内存管理是编译器的工作,但是附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。这一点在使用时要注意。

“不会持有对象” 这一特点使它和__weak的作用相似,可以防止循环引用。 “不安全“ 这一特点是它和__weak的区别,那么它不安全在哪呢?

id __weak weakObj = nil;
id __unsafe_unretained uuObj = nil;
{
    id __strong strongObj = [[NSObject alloc] init];
    weakObj = strongObj;
    unsafeUnretainedObj = strongObj;
    NSLog(@"strongObj:%@", strongObj);
    NSLog(@"weakObj:%@", weakObj);
    NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj);
}
NSLog(@"-----obj dealloc-----");
NSLog(@"weakObj:%@", weakObj);
NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj); // Crash:EXC_BAD_ACCESS

/*
strongObj:<NSObject: 0x6000038f4340>
weakObj:<NSObject: 0x6000038f4340>
unsafeUnretainedObj:<NSObject: 0x6000038f4340>
-----obj dealloc-----
weakObj:(null)
(lldb) 
*/

以上代码运行崩溃。原因是__unsafe_unretained修饰的对象在被销毁之后,指针仍然指向原对象地址,我们称它为 “悬垂指针”。这时候如果继续通过指针访问原对象的话,就会导致Crash。而__weak修饰的对象在被释放之后,会将指向该对象的所有__weak指针变量全都置为nil。这就是__unsafe_unretained不安全的原因。所以,在使用__unsafe_unretained修饰符修饰的对象时,需要确保它未被销毁。

Q: 既然 __weak 更安全,那么为什么已经有了 __weak 还要保留 __unsafe_unretained ?

__weak仅在ARC中才能使用,而MRC只能使用__unsafe_unretained; __unsafe_unretained主要跟 C 代码交互; __weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained比__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。 A 持有 B 对象,当 A 销毁时 B 也销毁。这样当 B 存在,A 就一定会存在。而 B 又要调用 A 的接口时,B 就可以存储 A 的__unsafe_unretained指针。 比如,MyViewController 持有 MyView,MyView 需要调用 MyViewController 的接口。MyView 中就可以存储__unsafe_unretained MyViewController *_viewController。 虽然这种性能上的提升是很微小的。但当你很清楚这种情况下,__unsafe_unretained也是安全的,自然可以快一点就是一点。而当情况不确定的时候,应该优先选用__weak。

__autoreleasing

Autoreleasepool(自动释放池)

首先讲一下自动释放池,在ARC下已经禁止使用NSAutoreleasePool类创建自动释放池,而用@autoreleasepool替代。

  • MRC下可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release]; // [pool drain]

Q: 释放NSAutoreleasePool对象,使用[pool release]与[pool drain]的区别? Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下[pool release]和[pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)

  • ARC下只能使用@autoreleasepool。
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}
__autoreleasing 使用

在MRC中我们可以给对象发送autorelease消息来将它注册到autoreleasepool中。而在ARC中autorelease已禁止调用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到autoreleasepool中。

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

以上代码在MRC中等价于:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
// 或者
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}