iPhone开发之深入浅出 (3) — ARC之前世今生

本文由易飞扬的博客出品,泰然授权转载,如需转载请联系原版权方。

原文链接:http://www.yifeiyang.net/development-of-the-iphone-simply-3/
前两节我们对 ARC(Automatic Reference Counting) 有了一个基本的理解,但是 ARC 是怎么产生的,为什么苹果要在其最新的 iOS/Mac OS X 上导入该框架? 如果不理解其背后的基本原理,只是死记硬背那些规则/方法,是毫无意义的。就像我们从小接受的填鸭式教育,基本上到后来都还给老师了。

本节,我们先来看看 ARC 产生之前的 Objective-C 内存管理世界,然后再来看看导入 ARC 后,新的 LLVM 编译器在背后为我们做了什么。

Objective-C 内存管理

和许多面向对象语言一样,Objective-C 中内存管理的方式其实就是指 引用计数 (Reference Counting)的使用准则。如下图所示,对象生成的时候必定被某个持有者拿着,如果有多个持有者的话,其引用计数就会递增;相反失去一个持有者那么引用计数即会递减,直到失去所有的持有者,才真正地从内测中释放自己。







基本原则

内存管理的依循下面的基本原则
  • 自己生成的对象,那么既是其持有者
  • 不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)
  • 如果不想持有对象的时候,必须释放其所有权
  • 不能释放已不再持有所有权的对象

结合 Objective-C 语言中的方法,我们来看看基本的内存管理。

方法 动作
alloc/new/copy/mutableCopy 生成对象并拥有所有权
retain 拥有对象所有权
release 释放对象所有权
dealloc 释放对象资源

实际上这些函数并不能说是 Objective-C 语言所特有的,而是 OS X / iOS 系统库中包含的基类函数;具体说就是 Cocoa Framework::Foundation::NSObject 基类的成员函数。

Objective-C 语言内部严格遵守上面表格中的定义;首先是 alloc/new/copy/mutableCopy 这几个函数,并且是alloc/new/copy/mutableCopy 开头的函数,比如:allpcMyObject/newTheObject/copyThis/mutableCopyTheObject 等都必须遵循这个原则。

反而言之,如果不是 alloc/new/copy/mutableCopy 开头的函数,而且要返回对象的话,那么调用端只是生成对象,而不是其持有者。

-(id)allocObject {
    /*
     * 生成对象并拥有所有权
     */
    id obj = [[NSObject alloc] init];

    /*
     * 自己一直是持有对象状态
     */
    return obj;
}

如上面的例子,alloc 生成的对象,其所有权会传递给函数的调用端;即满足了 alloc 开头函数的命名规则。

再看下面的例子
-(id)object {
    id obj = [[NSObject alloc] init];

    /*
     * 自己一直是持有对象状态
     */

    [obj autorelease];

    /*
     * 对象还存在,只是并不持有它的所有权
     */

    return obj;
}

这里我们用到了 autorelease 函数。它的作用既是将对象放入 NSAutoreleasePool 中,由其来维护其生命周期。换句话说对象的持有者是 NSAutoreleasePool;上面的例子中,object 返回后,调用者将不持有其所有权。(除非再调用 retain。)

用 autorelease 的一个理由既是让程序员来控制对象的存活周期,而不像 C/C++ 等语言中,出栈后,栈中数据都被自动废弃,或者用 { } 框住的自动变量,当出了范围就看不到了。在 Objective-C 中,只有当 [pool drain] 被调用的时候,才清空 pool 中所有登录的对象实体,在这之前,你可以像往常一样正常使用对象。

当然可以想象得到的,如果一个程序只有一个 NSAutoreleasePool,并在 main 中声明,程序结束时才 [pool drain]/[pool release] 的话,那么所有 autorelease 的对象都将塞满这个 pool,会耗掉系统大部分内存。所以,使用 NSAutoreleasePool 的时候也尽量建议局部使用,比如下面的循环。

for (i=0; i < 100; i++) {
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    // 下面的函数由于不属于 alloc/new/copy/mutableCopy 范畴的函数,所以都使用了 autorelease
    NSMutableArray* array = [NSMutableArray array];
    NSString *str = [NSString stringWithFormat:@"TestCode"];

    /*
     * 其他使用autorelease定义的对象
     */
    Test *test = [[[Test alloc] init] autorelease];

    // 通过下面的函数,可以随时监控pool中的对象
    // iOS以外的运行库的情况下,也可以使用 _objc_autoreleasePoolPrint() 私有函数,只是需要下面的声明
    // extern void _objc_autoreleasePoolPrint();
    [NSAutoreleasePool showPools];

    // 这里把所有pool中的对象都释放掉
    [pool release];
}

当然 NSAutoreleasePool 也可以嵌套,基本上都依存大括号规则。

编程准则

基于以上原则,在 ARC 诞生之前,我们往往用下面准则来写代码。

生成对象时,使用autorelease

一般情况下,我们这样生成对象并使用

MyController* controller = [[MyController alloc] init];
// ......
[controller release];

如果在 [controller release] 之前函数return了怎么样,内存泄露了不是;为了防患于未然,一般像下面一样 生成对象时,使用autorelease。这样一来,该对象就被自动加入到最近的那个 pool 中。

MyController* controller = [[[MyController alloc] init] autorelease];
对象代入时,先autorelease后再retain

对象代入的时候,如果之前不将变量所持有的对象释放,那么很可能引起内存泄露。比如下面的代码

{
  _member = [[TempValue alloc] init];
}

- (void)setValue:(TempValue *)value {
  _member = value;
  // 这时,之前持有的对象因为没有 release 而引起内存泄露
  // 当然,先 [_member release] 后再代入也是可以的,
  // 但是当与「对象在函数中返回时」的问题一同考虑时,
  // 如果没有 return [[object retain] autorelease] 的保证,这里即使 [_member release]也是百搭
  // 详细的解释见下
}

鉴于以上原因,我们将原先的对象先autorelease后再将新对象retain代入。

{
  _member = [[TempValue alloc] init];
  // 这里,即使使用【生成对象时,使用autorelease】的准则,也没有关系
  // 使用autorelease一次就将制定对象放入pool中,放几次[pool drain]的时候就释放几次
}

- (void)setValue:(TempValue *)value {
  [_member autorelease];
  _member = [value retain];
}

该原则遵循 Failed Self 的原则,虽然从性能上看有所损耗但是保证了代码质量。

对象在函数中返回时,使用return [[object retain] autorelease]

严格地说,是除 alloc/new/copy/mutableCopy 开头函数以外的函数中,有对象放回时,使用return [[object retain] autorelease]。

我们结合下面的例子来说明,并总结出该问题的几种解决方案
@implementation FooClass

- (void)setObject:(MyObject *)object;
 {
     // 这里故意没有使用 autorelease,以便说明问题
     [_object release];
     _object = [object retain];
 }

- (id)object;
 {
     return _object;
 }

- (void)dealloc;
 {
     [_object release];
     [super dealloc];
 }

@end

@implementation BarClass

- (void)doStuff;
 {
    FooClass * foo = [[FooClass alloc] init];

    // 创建第一个对象,引用计数 = 1
    MyObject * firstObject = [[MyObject alloc] init];
    // setObject中由于 [object retain] ,引用计数 = 2
    [foo setObject:firstObject];
    // 释放一次,引用计数 = 1;这之后对象有正确的所有权属性
    [firstObject release];

    // 通过非 alloc/new/copy/mutableCopy 开头函数得到对象
    // anObject 指向第一个对象,但是并没有其所有权,对象引用计数 = 1
    MyObject * anObject = [foo object];
    [anObject testMethod];

    // 创建第二个对象
    MyObject * secondObject = [[MyObject alloc] init];
    // setObject中由于 [_object release]; 第一个对象引用计数 = 0,内存被释放
    [foo setObject:secondObject];
    [secondObject release];

    // 程序在这里崩溃了,因为 anObject 指向了一个空地址
    [anObject testMethod];
}

@end

从结论我们来看看该问题的几种可行的解决方案;各种方案中没有列出的代码与原先代码一致。

生成对象时,使用autorelease
@implementation BarClass

- (void)doStuff;
 {
    FooClass * foo = [[FooClass alloc] init];

    MyObject * firstObject = [[[MyObject alloc] init] autorelease];
    [foo setObject:firstObject];

    MyObject * anObject = [foo object];
    [anObject testMethod];

    MyObject * secondObject = [[[MyObject alloc] init] autorelease];
    [foo setObject:secondObject];

    [anObject testMethod];
}

@end

对象生成时,即被放入最近的 pool 中,不需要人为特殊的维护,对象的生命周期将被延续,出 {} 范围之时即对象释放之际。

对象代入时,先autorelease后再retain
- (void)setObject:(MyObject *)object;
 {
     [_object autorelease];
     _object = [object retain];
 }

- (id)object;
 {
     // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则
     return _object;
 }

同样的,对象被放入最近的 pool 中,第二次 setObject 后对象引用计数仍为1, pool 清空时才执行最后一次对象release,从而保证了代码的正确性。

对象在函数中返回时,使用return [[object retain] autorelease];
- (void)setObject:(MyObject *)object;
 {
     [_object release];
     _object = [object retain];
 }

- (id)object;
 {
     // 遵循非 alloc/new/copy/mutableCopy 开头的函数,不赐予所有权原则
     return [[_object retain] autorelease];
 }

好不容易回到了本小节要说明的方法;可以看到这是从另一个角度解决了该问题:[foo object] 的时候保证引用计数是2,并将对象放入pool中维护。

总结上面上面3中方法,虽说是从不同角度入手解决了这个问题,但是基本原则不变,利用 NSAutoreleasePool 机制帮程序员维护代码,管理内存。

如果你觉得3种编码原则怎么搭配使用,在什么样的场合下选择比较麻烦,不要紧,都用就得了。我们牺牲的只是 NSAutoreleasePool 中的一些内存,一小许性能损失罢了,这总比我们的程序崩溃了强。

ARC 诞生

ARC 是什么我不需要再解释,若有不明白,可以看iPhone开发之深入浅出 (1) — ARC是什么。

ARC 严格遵守 Objective-C 内存管理的基本原则
  • 自己生成的对象,那么既是其持有者
  • 不是自己生成的对象,也可成为其持有者(一个对象可以被多个人持有)
  • 如果不想持有对象的时候,必须释放其所有权
  • 不能释放已不再持有所有权的对象

并从编译器角度维护了该原则,比如如果不是 alloc/new/copy/mutableCopy 开头的函数,编译器会将生成的对象自动放入 autoReleasePool 中。如果是 __strong 修饰的变量,编译器会自动给其加上所有权。等等,详细,我们根据不同的关键字来看看编译器为我们具体做了什么。并从中总结出 ARC 的使用规则。

__strong

我们先来看看用 __strong 修饰的变量,以及缺省隐藏的 __strong 情况。

{
    /*
     * 生成对象并拥有所有权
     */
    id __strong obj = [[NSObject alloc] init];

    /*
     * 自己一直是持有对象状态
     */
}
    /*
     * 变量出生命周期时,失去全部所有者,对象内存空间被释放
     */

这种情况毫无悬念,缺省使用 alloc/new/copy/mutableCopy 开头的函数也是这样的结果。并且在这里,编译器帮我们自动的调用了对象的 release 函数,不需要手工维护。再看看下面的情况。

{
    /*
     * 生成对象但是并没有其所有权
     */
    id __strong obj = [NSMutableArray array];

    /*
     * 由于变量声明是强引用,自己一直是持有对象状态
     * 编译器根据函数名,再将该对象放入 autoreleasepool 中
     */
}
    /*
     * 变量出生命周期时,失去全部所有者,对象内存空间被释放
     */

由上,虽然不是用 alloc/new/copy/mutableCopy 开头的函数得到的对象,由于是强参照,我们任然成为对象的持有者。而这,正是编译器帮我们做到的。

具体做的是什么呢?其实就是【对象在函数中返回时,使用return [[object retain] autorelease]】所描述的;如果你反汇编一下ARC生成的代码,可以看到这时会自动调用名为 objc_retainAutoreleaseReturnValue 的函数,而其作用和 [[object retain] autorelease] 一致。编译器通过函数名分析,如果不是 alloc/new/copy/mutableCopy 开头的函数,自动加入了这段代码。

另外,缺省 __strong 修饰的变量,对象代入的时候也正确地保证对象所有者规则;代入新对象时,自动释放旧对象的参照,代入nil的时候,表示释放当前对象的强参照。

__weak

虽然大部分场合,大部分问题使用 __strong 来编码就足够了;但是为了解决循环参照的问题 __weak 关键字修饰【弱参照】变量就发挥了左右。关于循环参照的问题,准备在以后的博文中介绍;今天,主要看看编译器在背后怎么处理 __weak 变量的。

__weak 声明的变量其实是被放入一个weak表中,该表和引用计数的表格类似,是一个Hash表,都是以对象的内存地址做key,同时,针对一个对象地址的key,可以同时对应多个变量的地址。

当一个 __weak 所指对象被释放时,系统按下面步骤来处理
  • 从weak表中,通过对象地址(key)找到entry
  • 将entry中所有指向该对象的变量设为nil
  • 从weak表中删除该entry
  • 从对象引用计数表中删除对象entry(通过通过对象地址找到)

另外,当使用 __weak 修饰的变量的时候,变量将放入 autoreleasepool 中,并且用几次放几次。比如下面的简单例子。

{
    id __weak o = obj;
    NSLog(@"1 %@", o);
    NSLog(@"2 %@", o);
    NSLog(@"3 %@", o);
    NSLog(@"4 %@", o);
    NSLog(@"5 %@", o);
}

这里我们用了5次,那么pool中就被登录了5次;从效率上考虑这样当然不是很好,可以通过代入 __strong 修饰的强参照变量来避开这个问题。

{
    id __weak o = obj;
    id temp = o;
    NSLog(@"1 %@", temp);
    NSLog(@"2 %@", temp);
    NSLog(@"3 %@", temp);
    NSLog(@"4 %@", temp);
    NSLog(@"5 %@", temp);
}

另外,还有通过重载 allowsWeakReference/retainWeakReference 函数来限制 __weak 声明变量使用回数的方法,毕竟不在本次讨论范畴之内,就此省略。

话说回来,为什么使用弱参照变量的时候,要将其放入 autoreleasepool 中呢?想想弱参照的定义就应该明白了 —- 如果在访问弱参照对象时,该对象被释放了怎么办,程序不就崩溃了嘛;所以为了解决该问题,又再一次用到了 pool。

__autoreleasing

虽然上面还没有降到该关键字,但是编译器在很多时候已经用到了 autoreleasepool。比如非 alloc/new/copy/mutableCopy 开头的函数返回一个对象的时候,又比如使用一个 __weak 声明的变量的时候。

实际上,写ARC代码的时候,明示 __autoreleasing 声明变量和明示 __strong 声明变量一样基本上没有,因为编译器已经为我们做了很多,很智能了(前提是我们要按ARC的规则写代码)。

还有一种编译器缺省使用 __autoreleasing 关键字声明变量的时候:对象指针类型。比如下面的对应关系。

   id *obj == id __autoreleasing *obj
   NSObject **obj == NSObject * __autoreleasing *obj

所以,下面两个函数的是等价的。

-(BOOL)performOperationWithError:(NSError **)error;

-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

像下面的函数调用,为什么是可行的呢?

NSError __strong *error = nil;
BOOL result = [obj performOperationWithError:&error];

其实,编译器是这样解释这段代码的。

NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
error = tmp;

那么我们这样声明函数不就可以了吗?

-(BOOL)performOperationWithError:(NSError * __strong *)error;

答案是肯定的,你可以这样做,编译是可以通过,但你违反了非 alloc/new/copy/mutableCopy 开头的函数,不返回对象持有权的原则。这里是没有问题了,但也许影响到其他地方NG。

ARC 规则

结合上面的讲解,我想你也应该能够总结出来使用ARC时的规则

(这里只列出本讲中涉及的内容,其他的内容以后总结)

  • 代码中不能使用retain, release, retain, autorelease
  • 不能使用NSAllocateObject, NSDeallocateObject
  • 不能使用NSAutoReleasePool、而需要@autoreleasepool块
  • 严守内存管理相关函数命名规则

关于函数命名,伴随ARC的导入,还有一系列函数的定义也被严格定义了,那就是以 init 开头的函数。init 函数作为alloc生成对象的初期化函数,需要按原样直接传递对象给调用段,所以下面的声明是OK的。

-(id)initWithObject:(id)obj;

而下面的是NG的。

-(void)initWithObject;

不过声明为 -(void) initialize; 是没有问题的。

标签: iphone开发

?>