《Objective-C 高级编程》读书笔记之 ARC

前言

本文作为《Objective-C 高级编程》读书笔记的第一篇,给大家带来的是关于 ARC(Automatic Reference Counting)自动引用计数的知识点总结。


概念

顾名思义,ARC(Automatic Reference Counting)— 自动引用计数,是指内存管理中对引用采取自动计数的技术。以下摘自苹果的官方文档:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects. Rather than having to think about retain and release operations, ARC allows you to concentrate on the interesting code, the object graphs, and the relationships between objects in your application.

一句话总结就是:在 LLVM 编译器中设置 ARC 为有效状态,就无需再次键入 retain 或者是 release 代码。


MRC

在介绍 ARC 之前,不得不提 MRC (MannulReference Counting) — 手动引用计数,即指内存管理中对引用采取手动计数的技术。

引用计数

我们需要知道 Objective-C 采用的是引用计数式的内存管理方式,这一方式的特点是:

  1. 自己生成的对象自己持有。比如:NSObject * __strong object = [NSObject alloc] init];
  2. 非自己生成的对象,自己也能持有。比如:NSMutableArray * __strong array = [NSMutableArray array];
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象自己无法释放

具体可参见下表:

对象操作 OC 中对应的方法 对应的 retainCount 变化
生成并持有对象 alloc/new/copy/mutableCopy等 +1
持有对象 retain +1
释放对象 release -1
废弃对象 dealloc -

注意:这些有关 OC 内存管理的方法,实际上不包括在该语言中,而是包含再 Cocoa 框架中用于 OS X、iOS 应用开发。Cocoa 框架中 Foundation 框架类库的 NSObject 类担负内存管理的职责。Objective-C 内存管理中的 alloc/retain/release/dealloc 方法分别指代 NSObject 类的 alloc 类方法、retain 实例方法、release 实例方法和 dealloc 实例方法。

(图片来自)

alloc/retain/release/dealloc 实现

苹果的实现大概就是采用散列表(引用计数表)来管理引用计数,如下图所示:

(图片来自)

通过引用计数表管理引用计数的好处如下:

  1. 对用内存块的分配无需考虑内存块头部
  2. 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块

这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置,如下图所示:

(图片来自)

另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。


ARC

当当当~~~今天的主角登场啦!!!

实际上引用计数式内存管理的本质部分在 ARC 中并没有改变。就像自动引用计数这个名称表示的那样,ARC 只是自动地帮助我们处理引用计数的相关部分。

所有权修饰符

Objective-C 编程中为了处理对象,可将变量类型定义为 id 类型或各种对象类型。

所谓对象类型就是指向 NSObject 这样的 Objective-C 类的指针,例如 NSObject 。id 类型用于隐藏对象类型的类名部分,相当于 C 语言中常用的 void

ARC 有效时,id 类型和对象类型同 C 语言其他类型不同,其类型上必须附加所有权修饰符。所有权修饰符一共有 4 种:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

说到变量所有权修饰符,有人可能会跟属性修饰符搞混,这里做一个对照关系小结:

  • assign 对应的所有权类型是 __unsafe_unretained
  • copy 对应的所有权类型是 __strong
  • retain 对应的所有权类型是 __strong
  • strong 对应的所有权类型是 __strong
  • unsafe_unretained 对应的所有权类型是 __unsafe_unretained
  • weak 对应的所有权类型是 __weak

以上除了 weak 外,其他的属性修饰符在 MRC 模式下也是有效的。

另外,__strong__weak__autoreleasing 修饰的自动变量会自动初始化为 nil。

__strong 修饰符

__strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

__weak 修饰符

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。__weak 最常见的一个作用就是用来避免强引用循环。但是需要注意的是,__weak 修饰符只能用于 iOS 5 以上的版本,在 iOS 4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

循环引用


__weak 实现

关于 runtime 如何实现 weak 属性,可参见:《招聘一个靠谱的 iOS》 中的解答。

__unsafe_unretained 修饰符

ARC 是在 iOS 5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在 ARC 刚发布时兼容 iOS 4 以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义 property 时对应的是 unsafe_unretained__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount + 1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

__autoreleasing 修饰符

在 ARC 模式下,我们不能显示的使用 autorelease 方法了,但是 autorelease 的机制还是有效的,通过将对象赋给 __autoreleasing 修饰的变量就能达到在 MRC 模式下调用对象的 autorelease 方法同样的效果。

也就是说可以理解为,在 ARC 有效时,用 @autoreleasepool 块替代 NSAutoreleasePool 类,用附有 __autoreleasing 修饰符的变量替代 autorelease 方法,如图所示:

在 ARC 模式下,显式的使用 __autoreleasing 的场景很少见,但是 autorelease 的机制却依然在很多地方默默起着作用。我们来看看这些场景:

  1. 方法返回值
  2. 访问 __weak 修饰的变量
  3. id 的指针或对象的指针(id *)

关于使用场景的具体讲解,感兴趣的同学可以自学搜索。

Autorelease Pool

在 ARC 下,我们并不需要手动调用 autorelease 有关的方法,甚至可以完全不知道 autorelease 的存在,就可以正确管理好内存。因为 Cocoa Touch 的 Runloop 中,每个 runloop circle 中系统都自动加入了 Autorelease Pool 的创建和释放。

Autorelase Pool 提供了一种可以允许你向一个对象延迟发送 release 消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。

所谓的延迟发送 release 消息指的是,当我们把一个对象标记为 autorelease 时:

1
NSString* str = [[[NSString alloc] initWithString:@"hello"] autorelease];

这个对象的 retainCount 会 + 1,但是并不会发生 release。当这段语句所处的 autoreleasepool 进行 drain 操作时,所有标记了 autorelease 的对象的 retainCount 会被 - 1。即 release 消息的发送被延迟到 pool 释放的时候了。

在 ARC 环境下,苹果引入了 @autoreleasepool 语法,不再需要手动调用 autoreleasedrain 等方法。

规则

在 ARC 有效的情况下编译源代码,必须遵守一定的规则。下面就是具体的 ARC 的规则:

  1. 不能显式使用 retain/release/retainCount/autorelease

  2. 不能使用 NSAllocateObject/NSDeallocateObject

  3. 需要遵守内存管理的方法命名规则。在 ARC 模式和 MRC 模式下,以 alloc/new/copy/mutableCopy 开头的方法在返回对象时都必须返回给调用方所应当持有的对象。在 ARC 模式下,追加一条:以 init 开头的方法必须是实例方法并且必须要返回对象。返回的对象应为 id 类型或声明该方法的类的对象类型,或是该类的超类型或子类型。该返回的对象并不注册到 Autorelease Pool 中,基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象。需要注意的是:- (void)initialize; 方法虽然是以 init 开头但是并不包含在上述规则中

  4. 不要显式调用 dealloc

  5. 使用 @autoreleasepool 块替代 NSAutoreleasePool

  6. 不能使用区域(NSZone)

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

  8. 显式转换 id 和 void *

Toll-Free Bridge

There are a number of data types in the Core Foundation framework and the Foundation framework that can be used interchangeably. This capability, called toll-free bridging, means that you can use the same data type as the parameter to a Core Foundation function call or as the receiver of an Objective-C message. For example, NSLocale (see NSLocale Class Reference) is interchangeable with its Core Foundation counterpart, CFLocale (see CFLocale Reference). Therefore, in a method where you see an NSLocale * parameter, you can pass a CFLocaleRef, and in a function where you see a CFLocaleRef parameter, you can pass an NSLocale instance. You cast one type to the other to suppress compiler warnings, as illustrated in the following example.

Toll-Free Briding 保证了在程序中,可以方便和谐的使用 Core Foundation 类型的对象和 Objective-C 类型的对象。

在 MRC 时代,由于 Objective-C 类型的对象和 Core Foundation 类型的对象都是相同的 release 和 retain 操作规则,所以 Toll-Free Bridging 的使用比较简单,但是自从切换到 ARC 后,Objective-C 类型的对象内存管理规则改变了,而 Core Foundation 依然是之前的机制,换句话说,Core Foundation 不支持 ARC。

这个时候就必须要要考虑一个问题了,在做 Core Foundation 与 Objective-C 类型转换的时候,用哪一种规则来管理对象的内存。显然,对于同一个对象,我们不能够同时用两种规则来管理,所以这里就必须要确定一件事情:哪些对象用 Objective-C(也就是 ARC)的规则,哪些对象用 Core Foundation 的规则(也就是 MRC)的规则。或者说要确定对象类型转换了之后,内存管理的 ownership 的改变。于是苹果在引入 ARC 之后对 Toll-Free Bridging 的操作也加入了对应的方法与修饰符,用来指明用哪种规则管理内存,或者说是内存管理权的归属。这些方法和修饰符分别是:

  • __bridge(修饰符)
  • __bridge_retained(修饰符) or CFBridgingRetain(函数)
  • __bridge_transfer(修饰符) or CFBridgingRelease(函数)

本文关于 Toll-Free Bridge 不做过多介绍,感兴趣的同学可以参见:Toll-Free Bridge


要点

  1. ARC 是编译器提供的机制,而不是 GC (Garbage Collection) 这种运行时提供的机制

  2. autorelease 实例方法的本质就是调用 NSAutoreleasePool 对象的 addObject 类方法


参考

我知道是不会有人点的,但万一有人想不开呢?