首页 关于 微信公众号
欢迎关注我的微信公众号

iOS ARC 内存管理要点

前言

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

而 ARC 则是帮助我们做对象内存管理的一套机制,使得我们以前在 MRC 模式下管理内存工作量能在 ARC 模式下得到缓解。正如苹果官方文档上所描述的:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects.

可见 ARC 是编译时特性,它没有改变 Objective-C 引用计数式内存管理的本质,更不是 GC(垃圾回收)。

在 ARC 特性下有 4 种与内存管理息息相关的变量所有权修饰符值得我们关注:

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

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

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

接下来就一一介绍 4 种变量所有权修饰符。

关于 __strong

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

关于 __weak

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

__weak 的几个使用场景:

关于 __autoreleasing

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

__autoreleasing 修饰的对象会被注册到 Autorelease Pool 中,并在 Autorelease Pool 销毁时被释放,和 MRC 特性下的 autorelease 的意义相同。定义 property 时不能使用这个修饰符,因为任何一个对象的 property 都不应该是 autorelease 类型的。

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

方法返回值

示例

首先,我们看这个方法:

- (NSObject *)object {
    NSObject *o = [[NSObject alloc] init];

    return o;
}

这里 o 的所有权修饰符是默认的 __strong。由于 return 使得 o 超出其作用域,它强引用持有的对象本该被释放,但是由于该对象作为函数返回值,所以一般情况下编译器会自动将其注册到 Autorelease Pool 中(注意这里是一般情况下,在一些特定情况下,ARC 机制提出了巧妙的运行时优化方案来跳过 autorelease 机制,见后面章节)。这是 autorelease 机制默默起作用的一个例子。

方法返回值时的 autorelease 机制

那么这里有一个问题:为什么方法返回值的时候需要用到 autorelease 机制呢?

这涉及到两个角色的问题。一个角色是调用方法接收返回值的接收方。当参数被作为返回值 return 之后,接收方如果要接着使用它就需要强引用它,使它 retainCount +1,用完后再清理,使它 retainCount -1。有持有就有清理,这是接收方的责任。另一个角色就是返回对象的方法,即提供方。在方法中创建了对象并作为返回值时,一方面你创建了这个对象你就得负责释放它,有创建就有释放,这是创建者的责任。另一方面你得保证返回时对象没被释放以便方法外的接收方能拿到有效的对象,否则你返回的是 nil,有何意义呢。所以就需要找一个合理的机制既能延长这个对象的生命周期,又能保证对其释放。这个机制就是 autorelease 机制

当对象作为参数从方法返回时,会被放到正在使用的 Autorelease Pool 中,由这个 Autorelease Pool 强引用这个对象而不是立即释放,从而延长了对象的生命周期,Autorelease Pool 自己销毁的时候会把它里面的对象都顺手清理掉,从而保证了对象会被释放。但是这里也引出另一个问题:既然会延长对象的生命周期到 Autorelease Pool 被销毁的时候,那么 Autorelease Pool 的生命周期是多久呢?会不会在 Autorelease Pool 都销毁了,接收方还没接收到对象呢?

Autorelease Pool 是与线程一一映射的,这就是说一个 autoreleased 的对象的延迟释放是发生在它所在的 Autorelease Pool 对应的线程上的。因此,在方法返回值的这个场景中,如果 Autorelease Pool 的 drain 方法没有在接收方和提供方交接的过程中触发,那么 autoreleased 对象是不会被释放的(除非严重错乱的使用线程)。

通常,Autorelease Pool 的销毁会被安排在很好的时间点上:

至此,我们知道了为何方法返回值需要 autorelease 机制,以及这一机制是如何保障接收方能从提供方那里获得依然鲜活的对象。

关于 Autorelease Pool 可以看一下这几篇文章:

ARC 模式下方法返回值跳过 autorelease 机制的优化方案

在 MRC 时代,当我们自己创建了对象并把它作为方法的返回值返回出去时,需要手动调用对象的 autorelease 方法,如上节所讲的利用 autorelease 机制正确返回对象。到了 ARC 时代,ARC 需要保持对 MRC 代码的兼容,这就意味着 MRC 的实现和 ARC 的实现可以相互替换,而对象接收方和对象提供方无需知道对方是 MRC 实现还是 ARC 实现也能正确工作。比如,当基于 MRC 实现的代码调用你的一个 ARC 实现的方法来获取一个对象,那么你的方法必须同样采用上文所讲的 autorelease 机制来返回对象以确保对象接收方能正确获得对象。所以,即使在 ARC 模式下对象的 autorelease 方法不再能被显示调用,但是 autorelease 的机制仍然是在默默的工作着,只是编译器在帮你实践这一机制。

但是,ARC 还提出了巧妙的运行时优化方案来跳过 autorelease 机制。这个过程是这样的:当方法的调用方和实现方的代码都是基于 ARC 实现的时候,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 替代前面说的 autorelease。在调用方持有方法返回对象的时候(也就是做 retain 的时候),ARC 会调用 objc_retainAutoreleasedReturnValue()。在调用 objc_autoreleaseReturnValue() 时,它会在栈上查询 return address 来确定 return value 是否会被传给 objc_retainAutoreleasedReturnValue()。如果没传,那么它就会走前文所讲的 autorelease 的过程。如果传了(这表明返回值能顺利从提供方交接给接收方),那么它就跳过 autorelease 并同时修改 return address 来跳过 objc_retainAutoreleasedReturnValue(),从而一举消除了 autoreleaseretain 的过程。这个方案可以在 MRC-to-ARC 调用、ARC-to-ARC 调用以及 ARC-to-MRC 调用中正确工作,并在符合条件的一些 ARC-to-ARC 调用中消除 autorelease 机制。

访问 __weak 修饰的变量

在访问 __weak 修饰的变量时,实际上必定会访问注册到 Autorelease Pool 的对象。如下来年两段代码是相同的效果:

id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

为什么会这样呢?因为 __weak 修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。

id 的指针或对象的指针(id *)

另一个隐式地使用 __autoreleasing 的例子就是使用 id 的指针或对象的指针(id *) 的时候。

看一个最常见的例子:

NSError *__autoreleasing error; 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) { 
  NSLog(@"Error: %@", error); 
}

// 即使上面你没有写 __autoreleasing 来修饰 error,编译器也会帮你做下面的事情:
NSError *error; 
NSError *__autoreleasing tempError = error; // 编译器添加 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError]) 
{ 
  error = tempError; // 编译器添加 
  NSLog(@"Error: %@", error); 
}

error 对象在你调用的方法中被创建,然后被放到 Autorelease Pool 中,等到使用结束后随着 Autorelease Pool 的销毁而释放,所以函数外 error 对象的使用者不需要关心它的释放。

在 ARC 中,所有这种指针的指针类型(id *)的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing 类型。

有一点特别需要注意的是,某些类的方法会隐式地使用自己的 Autorelease Pool,在这种时候使用 __autoreleasing 类型要特别小心。比如 NSDictionary 的 enumerateKeysAndObjectsUsingBlock 方法:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

          // do stuff  
          if (there is some error && error != nil) {
                *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
          }

    }];
}

上面的代码中其实会隐式地创建一个 Autorelease Pool,类似于:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

          @autoreleasepool {  // 被隐式创建。
              if (there is some error && error != nil) {
                    *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
              }
          }
    }];

    // *error 在这里已经被dict的做枚举遍历时创建的 Autorelease Pool释放掉了。
} 

为了能够正常的使用 *error,我们需要一个 strong 类型的临时引用,在 dict 的枚举 Block 中是用这个临时引用,保证引用指向的对象不会在出了 dict 的枚举 Block 后被释放,正确的方式如下:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
  NSError * __block tempError; // 加 __block 保证可以在Block内被修改。
  [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 
    if (there is some error) { 
      *tempError = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil]; 
    }  

  }] 

  if (error != nil) { 
    *error = tempError; 
  } 
}

关于 __unsafe_unretained

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

__unsafe_unretained 的应用场景:

ARC 模式规则

ARC 模式下,还有一些需要注意的规则:

关于 Toll-Free Bridging

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

只是声明类型转变,但是不做内存管理规则的转变。

比如:

CFStringRef s1 = (__bridge CFStringRef) [[NSString alloc] initWithFormat:@"Hello, %@!", name];

只是做了 NSString 到 CFStringRef 的转化,但管理规则未变,依然要用 Objective-C 类型的 ARC 来管理 s1,你不能用 CFRelease() 去释放 s1。

__bridge_retained or CFBridgingRetain

表示将指针类型转变的同时,将内存管理的责任由原来的 Objective-C 交给Core Foundation 来处理,也就是,将 ARC 转变为 MRC。

比如,还是上面那个例子

NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
// or CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);
// do something with s2
//...
CFRelease(s2); // 注意要在使用结束后加这个

我们在第二行做了转化,这时内存管理规则由 ARC 变为了 MRC,我们需要手动的来管理 s2 的内存,而对于 s1,我们即使将其置为 nil,也不能释放内存。

__bridge_transfer or CFBridgingRelease

这个修饰符和函数的功能和上面那个 __bridge_retained 相反,它表示将管理的责任由 Core Foundation 转交给 Objective-C,即将管理方式由 MRC 转变为 ARC。

比如:

CFStringRef result = CFURLCreateStringByAddingPercentEscapes(. . .);
NSString *s = (__bridge_transfer NSString *)result;
//or NSString *s = (NSString *)CFBridgingRelease(result);
return s;

这里我们将 result 的管理责任交给了 ARC 来处理,我们就不需要再显式地将 CFRelease() 了。

对了,这里你可能会注意到一个细节,和 ARC 中那个 4 个主要的修饰符(__strong, __weak, …)不同,这里修饰符的位置是放在类型前面的,虽然官方文档中没有说明,但看官方的头文件可以知道。记得别把位置写错。

image

参考:

Blog

Opinion

Project