ARC 全称被各个网路博客安利这么多年也应该知道了(Auto Reference Counting),中文名就是 自动引用计数。这是一种比较简单的垃圾回收机制,什么是垃圾呢?就是堆分配过程中任何无法被变量或指针达到的地方叫做垃圾。这部分回收重新分配,叫做垃圾收集。

今天探讨一下引用计数这种垃圾收集在 objc 里面的实现(希望通过一篇野路子文章完整地知道 ARC实现的可以绕道了)。

垃圾是怎么产生的?

我们假设一个对象在内存里的布局是一个这样的结构体:

1
2
3
4
struct SomeObject {
char *firstName;
char *lastName;
}

它在初始化真正做的事是这个:

1
2
3
4
typedef struct SomeObject * SomeObjectRef;
SomeObjectRef obj= (SomeObjectRef)malloc(sizeof(struct SomeObject);
obj->firstName = (char *)malloc(10);
obj->lastName = (char *)malloc(15);

要记住,指针永远只能指向某个内存结构的首地址,那么它所能标记的只是一个类型大小的内存,而你从 malloc 申请的定长内存,永远只有首地址被标记到了。在弹栈的时候,你所用来标记结构体堆地址的指针(栈变量)毫无疑问能够随着弹栈交还它所占的空间,而那些分配在堆上的,被人遗忘后,谁也不知道它们“住在哪儿”,这一块儿内存不可复用,所以产生了“泄露”。要解决这些泄露,就是在弹栈前,写这样的代码:

1
2
3
free(obj->firstName);
free(obj->lastName);
free(obj);

有时为了避免野指针,还会在后面加上一个 obj = NULL;

很明显,一共写了7行代码,一半用在了管理内存上,不得不说这真是件非常繁琐的事情。并且,一个 objc对象的结构体远远比上文这个东西复杂得多,如果按照这样的方法,肯定会造成10行代码中一大半都是这种 “free”,简直令人头疼。

objc的引用类型和引用计数

让我们从刚才的 context中冷静一下。回想一下我们之前碰到的一个非典型的引用类型 Block 。我们之前有解释过它如何从一个字面量变为一个结构体再变为一个栈变量,最终引用或返回的过程中变为一个堆变量。它自身会有一个函数指针叫做 Block_copy,这个函数将一个 Block指针挪到堆上,并将 Block的引用计数加1.如果 Block已经在堆上了,直接将引用计数加1。对应的,还有一个函数指针叫做 Block_release,它负责把 Block的引用计数减1,当引用计数为0的时候,会将这个 Block从堆上销毁。

Block是一个非典型的引用类型,但和 objc的其他引用类型几乎一样,都靠引用计数来确定一个对象是否应该销毁。然后我们再想一下,objc对象的基本结构,在前文中我们也有提过,那么一个 objc对象的“Block_release”方法就可以对应地被抽象成 [NSObject release]方法。

现在好了,objc的引用类型(也叫 objective-c pointer types)都有了用来管理引用计数以及销毁的方法。这就形成了好几年前一直用的 MRC(手动引用计数)。不得不说引用计数是一个管理引用类型内存非常简单的方法,这不需要你像 mark-sweep那样专门分配一个内存用来索引需要被释放的对象,也不需要担心在启动 GC的时候停掉所有的程序,或者担心因为递归算法不够好,栈太深导致爆栈。因为引用计数管理的对象仅仅只做简单的加减法计算被引用的次数,在次数为0时调用销毁,只需要一个栈帧,并且不需要现有的程序停止。

从 MRC到 ARC

既然 MRC这么好,为什么还需要 ARC?

很显然,所有的用户并不是从 Assembly 和 C时代过来的。很可能他们的老师教的是 Java或 JavaScript这种语法简单且有完整 GC的编程语言。因为不是所有人都从计算机科学出身,很可能从软件工程管理或其他方面出身,本身更关注工程和效率。

确实在工程和效率这方面,C Family的编程语言确实拖着后腿。如果不是 Xcode帮你管理工程结构,你会发现写一个 Make文件就足以让你头疼。再深入一点,当你写着逻辑非常复杂的应用时,脑子里还要考虑着内存如何管理,这样的写出来的代码几乎是无法与其他人协同工作的。你可以回头看一遍 MRC时代一个 UIViewController的 property的 getter和 setter是如何写的。

既然受够了 MRC无尽的 retain/release,ARC也应运而生。ARC本质不是语言层面的产物,只是原本应该由你写的内存管理的代码由编译器为你代劳了。编译器的前端就会正确地猜测应该使用 retain/release的地方并且为你插入这些代码,从而免去了MRC中一遍又一遍重复的工作。

ARC相关的语法

不过很显然,仅仅靠猜测有时候编译器并不能帮你插入正确的 retain/release,所以 ARC还是有一些语法的。最显然的,你在声明 property的时候,有相关修饰属性的关键字:weak, assign, strong, copy, unsafe_unretained, retain。简单地看,它们表示的是当 property遇到那一块对象内存时的处理方法。还有一些标记语法,如:weak, unsafe_unretained等, 我想所有人都知道,就没必要啰嗦了。

不过还有一种内存管理上的问题,就是 一个 objc对象转向 C时,内存的归属权问题。这是一个很危险的问题,C是没有引用计数的,当一个 objc对象转换过来时,内存该由谁管理?若这个问题划分不清是一件很危险的事情。所以 objc的 bridge语法有几种:__bridge, __bridge_retained, _bridge_transfer。这是个很有趣的话题,__bridge代表着只获取 objc对象的指针,原来的引用计数啥样就啥样。那么获得 C指针的使用者应该注意不要在对象可能已经销毁的情况下用这个指针。 __bridge_retained表示获取 objc对象指针的同时将对象的引用计数加1,这就表示使用者在使用结束后应同时调用一次 release将引用计数减1,不然这个对象的引用计数很可能永远无法归零,不过这至少保证了使用者拿到这个指针时对象不会已经销毁了。__bridge_transfer就更有意思了,当objc对象被这样转换过来的时候,编译器不会在后续代码中为这个对象插入 ARC代码。也就是说引用计数完全交给了获取 C指针引用者管理,管理权被“移交”了。

ARC很“聪明”地通过以上的语法来告诉用户使用的注意项。

自动释放池

前文中我们有说当 [NSObject release]被调用时,引用计数为0了,对象会被释放,确实没错,释放后你再也用不到了。不过有时候,你需要一些延迟释放的操作,比如你弱引用了一个新生成的对象,总不能让这个对象一生成就在作用域结束时释放吧?那还有什么意义?可是你因为某种原因又不能强引用他,怎么办?目前,ARC的所有对象生成,几乎都会使用一种延迟释放的机制,叫做 autoreleasepool。

这是个啥玩意?从字面上看,叫自动释放池。

是干什么的?用于在一个合适的时机释放加入自动释放池里的自动管理内存对象。 (这种打官腔简直是放屁)

在 iOS App中,非常明显地从 main函数那边就可以看到,整个 AppDelegate都会处于一个自动释放池中。在后续的操作中,很多对象在初始化后都会加入离自己最近的一个自动释放池中。并且有意思的一点是,每一个 NSThread都会有一个缺省的自动释放池,所以在大多数线程中产生的自动释放对象都会加入到这个释放池。而且我们知道,NSThread是会有一个 RunLoop循环执行,自动释放池释放的实际也是在 RunLoop即将退出的那一刻,这就很巧妙地规避了垃圾回收机制工作时程序被停止的问题了:都没有任务在跑,回收一下垃圾怎么了?

那自动释放池到底是一个怎么样的东西?本质上它就是一个表结构的东西,每一个加入自动释放池的对象都会被它记录下地址(一个指针),在记录自动释放对象之前,这个表还会加入一个“哨兵”对象,这是分页管理存储中一个很有用的简单操作。因为在自动释放池进行释放操作时,需要避免操作不需要触及的区域。在 自动释放池嵌套时就更有用了,每一个自动释放池都只负责释放到自己的“哨兵”处。

虽然概念上自动释放池是个很简单的东西,但具体实现细节上反而有点复杂,我们也没必要去过多探究。因为你要了解它的原理也没什么用,能正确写 objc就好,如果你要造自己的 GC,干嘛要学这么落后的东西。

写在最后

如果能看我扯淡到这里,也说明你是一个脱离低级趣味的人了(因为这么无聊你还在看)。我非常好奇当初 iPhone OS是造了什么孽要被用 objc这种“大刑”伺候(知道的人请告诉我)。写写 C++不是挺好的吗?

而且很多Framework比如 CFNetwork, libclosure, JavascriptCore等等都是 C++实现的,最后反而包一层给 objc用,不是在开倒车么。

不过幸好,Swift这门用来替代 C++的语言正代表着未来。