ARC 的__autoreleasing相关知识点

引子

apple 的《Transitioning to ARC Release Notes》一文里,其中一小节"ARC Introduces New Lifetime Qualifiers"(ARC 引入新的生命周期限定符)中对 Variable Qualifiers(变量限定符)的说明,有一段代码引起了我的注意:
ARC 的__autoreleasing相关知识点_第1张图片
(NSError * __autoreleasing *)error

翻译过来是:
你也要注意通过引用方式传入的对象。以下代码会正常运转

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
   // Report the 
   // ...

而 error 的声明是隐式的:

NSError * __strong e;

方法的声明通常是:

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

因此编译器会重写代码:

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
   // Report the error.
   // ...

本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量。在获取__strong变量的地址时你可以通过将参数声明为 id __storng*来获得其原始指针。或者你可以将变量声明为 __autoreleasing。

问题来了:

  1. -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;中为什么需要用 __autoreleasing 变量限定符修饰?
  2. 本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量?

要解决上述两个问题,首先得知道

  1. __autoreleasing是什么?
  2. __autoreleasing作用什么?(为什么要使用__autoreleasing
  3. __autoreleasing使用注意点有哪些?

一、__autoreleasing 是什么?

从apple 的《Transitioning to ARC Release Notes》一文中,
"ARC Introduces New Lifetime Qualifiers"(ARC 引入新的生命周期限定符)里对 Variable Qualifiers(变量限定符)的说明
小节上可知:

__autoreleasing 是 ARC 下用于控制变量生命周期而引入的4个变量限定符之一。

  • 四个变量限定符为:
Variable Qualifier Desc other
__strong 是默认的。只要有强类型指针指向一个对象,那么该对象会一直”生存“下去。
__weak 表明一个不会维持所持对象生命期的引用。当没有强引用指向该对象时,弱引用会设置为nil。
__unsafe_unretained 指定一个引用,该引用不会维持所持对象的生命期,并且在没有强引用指向对象时也不会设置为nil。如果它所指向的对象已经被释放,那么它会成为一个野指针。
__autoreleasing 用以指示以引用(id*)传入的参数并在retun后自动释放。

二、__autoreleasing 作用什么?(为什么要使用__autoreleasing

从apple 的《Transitioning to ARC Release Notes》一文中,
ARC Enforces New Rules(ARC强制执行新规则)
小节上可知:

  1. 不能显示的调用dealloc,实现或调用 retain, release, retainCount,或 autorelease

  2. 不能使用 NSAutoreleasePool 对象

      ARC 提供了 @autoreleasepool来代替。这比 NSAutoreleasePool更高效。
    

对比一下 MRC 与 ARC 下使用 autoreleasepool 的不同地方:

/* MRC  */
1 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2 id obj = [[NSObject alloc] init];
3 [obj autorelease]; //对象调用 autorelease 方法就是将该对象注册到最近的 autoreleasepool 中
4 [pool drain];

而 ARC 下,可将上述代码写成下面这样:

/* ARC */
1 @autoreleasepool {
2 id __autoreleasing obj = [[NSObject alloc] init];
3 }

通过上述的对比可知:

  1. 可通过 @autoreleaepool块 来替代 NSNSAutoreleasePool类对象的生成、持有、以及废弃;

    1. ARC 的第1行代码 与 MRC 的第1行代码等价
    2. ARC 的第2行代码 与 MRC 的第2和3行代码等价
    3. ARC 的第3行代码 与 MRC 的第4行代码等价
    
  2. 通过将对象赋值给带有 __autoreleasing 修饰符的变量来代替调用 autorelease 方法,即将对象注册到 autoreleasepool;

因为不能使用 NSAutoreleasePool 和 调用 autorelease方法了,所以在给出一个对应的替换方案
使用 @autoreleaepool块对应 使用 NSAutoreleasePool
使用__autoreleasing 修饰符对应 调用 autorelease 方法

但是实际情况是:通常情况下不会显式地使用 __autoreleasing修饰符,就像不会显式地使用 __strong修饰符一样;
如下就是不显式使用__autoreleasing但对象同样被注册到 pool的3个例子
  1. 编译器会检查方法名是否以alloc, new, copy, mutableCopy 开始,如果不是则自动将返回值的对象注册到 autoreleasepool 中;

《Obejctive-C 高级编程 iOS 与 OS X 多线程和内存管理》一书中对内存的管理思想描述如下:

自己生成的对象,自己所持有。
非自己生成的对象,自己也能持有。(MRC:让对象调用 retain 方法,ARC:有__strong限定符)
不需要自己持有的对象是释放。
非自己持有的对象无法释放。

判断是否自己生成的对象,自己所持有规则有

1. 使用 alloc, new, copy, mutableCopy 开头的方法意味着:自己生成的对象,自己持有。
2. 使用 alloc, new, copy, mutableCopy 以外的方法取得的对象为:非自己生成并持有。

基于上述的理解后,下面代码中木有显式调用__autoreleasing,但生成的对象同样被注册到 pool 中:

@autoreleasepool {
    id __strong obj = [NSMutableArray array];
}

因为NSMutableArray的类方法array不是使用 alloc, new, copy, mutableCopy 开头的方法,产生的对象在编译判断方法名后自动注册到 autoreleasepool 中。

这个描述已不太正确的,但就目前文章要解决的问题来说足够了,相关描述请参见《理解AutoRelease Pool》的最后一节“最后一个问题”,这也是编译做了优化导致的情况之一)

以下为“取得非自己生成并持有对象”的方法(+array)事例代码

+ (instancetype)array {
    return [NSMutableArray array];
}

上述代码可写成:

+ (instancetype)array {
  id obj = [NSMutableArray array];
  return obj;
}

在 ARC 下,因为__strong 为默认修饰符,所以 id obj 与 id __strong obj 等价的。由于 return 使得对象变量超出其变量作用域,所以该强引用对应的自己持有的对象会被自动释放,**但该对象作为函数的返回值,编译器会自动将其注册到 autoreleasepool 中。
那大家思考一下如下这段代码:

@autoreleasepool {
  id obj = [[NSMutableArray alloc] init];
}
//上面init的对象是否被添加到 autoreleasepool 中? 

我自己的答案是:(如有错误欢迎指正)
被添加到autoreleasepool了,默认情况下要将 obj 指向的对象添加到autoreleasepool中是需要 __autoreleasing 修饰符去修饰 obj 的,那么ARC 下它应该就会进行一个编译转换,如:

id __autoreleasing tem = obj;
  1. 虽然__weak 修饰符是为了避免强引用循环(strong reference circle)而使用的,但在访问附有 __weak 修饰符的变量时,实际上必定要访问注册到 autoreleasepool 的对象。
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);

以下源码与上面相同

id __weak obj1 = obj0;
id __autoreleasing tem = obj1;
NSLog(@"class = %@", [tem class]);

因为 __weak 修饰符只持有对象的弱引用,而在访问引用对象过程中,该对象有可能被废弃。而将弱引用对象注册到 autoreleasepool 中,在 pool 被销毁之前都能确保该对象存在。

  1. id 的指针或对象的指针在没有显式地指定修饰符时候,会被默认附加上__autoreleasing 修饰符;
    如使用附有 __autoreleasing 修饰符的变量作为对象获得参数,与除 alloc/new/copy/mutableCopy外其他方法的返回值取得对象完全一样,都会注册到 autoreleasepool,并取得非自己生成并持有的对象。

有些情况下,我们是通过引用方式传入的对象,以获得更详细的情况,如:为了得到详细的错误信息,经常会在方法的参数中传递 NSError 对象指针

+ (nullable instancetype)stringWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error;

实际编译的时候,该方法中的error 参数会附加上__autoreleasing 修饰符

+ (nullable instancetype)stringWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError * __autoreleasing *)error;

假设我们有一个方法,方法声明如下

 - (BOOL)performOperationWithError:(NSError **)error;
//实际在编译时是这样的: - (BOOL)performOperationWithError:(NSError *__autoreleasing *)error;

该方法接受一个 NSError 对象的指针作为参数,返回一个 BOOL 值。使用时如下所示:

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

假设当发生错误时,方法内部动作如下:

- (BOOL)performOperationWithError:(NSError **)error {
    // 方法执行发生错误
    *error = [NSError errorWithDomain:MyAppDomain code:errorCode userInfo:nil];
    return NO;
}

对比之前提到的内存管理原则:

  1. 方法- (BOOL)performOperationWithError:(NSError *__autoreleasing *)error;不以alloc/new/copy/mutableCopy开头的;
  2. 声明为NSError *__autoreleasing *类型的 error 作为 *error被赋值;

以上两点都符合“非自己生成并持有对象”规则,所以 error 取得是:注册到 autoreleasepool,并取得非自己生成并持有的对象;

三、__autoreleasing使用注意点有哪些?

  1. 对象指针赋值时,所有权修饰符必须一致
//编译错误
NSError *error = nil;
NSError **pError = &error;

//编译正确
NSError *error = nil; // 默认修饰符是 __strong
NSError *__strong *pError = &error;

同样的:
NSError __weak *error = nil; 
NSError *__weak *pError = &error;//编译正确

NSError __unsafe_unretain *error = nil; 
NSError *__unsafe_unretain *pError = &error;//编译正确

按照上述,为啥下面的代码能够正常运行?

NSError *error1 = nil;
BOOL OK = [myObject performOperationWithError:&error];

例子中声明的是 NSError *error = nil; 默认修饰符 __strong, 而- (BOOL)performOperationWithError:(NSError *__autoreleasing *)error2;方法参数中使用的是__autoreleasing,传值时是 error2 = &error1,对象指针赋值,但所有权修饰符不一致,为什么没有报错呢?

这里就由文章开头的问题2说明了,针对这种“通过引用方式传入的对象”,编译器会重写代码,也就是本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量

NSError *error = nil;
NSError __autoreleasing *tmp = error; //此处为对象赋值
BOOL result = [obj performOperationWithError:&tmp];//传参时的动作才是对象指针赋值,需要保证所有权修饰符一致
error = tmp; //此处为对象赋值
  1. (参照以上3种不显式使用__autoreleasing,对象同样被注册到 autoreleasepool 的3个例子)虽然可以非显式使用__autoreleasing修饰符,但在显式地指定__autoreleasing修饰符时,必须要注意对象变量只能为自动变量(包括:局部变量、函数以及方法参数)

Ref

apple doc:

  1. Using Autorelease Pool Blocks
  2. Transitioning to ARC Release Notes

other doc:

  1. Objective-C 中的内存分配
  2. 文档翻译-Using Autorelease Pool Blocks
  3. 理解AutoRelease Pool
  4. 迁移至ARC版本说明(Transitioning to ARC Release Notes)
  5. 《Obejctive-C 高级编程 iOS 与 OS X 多线程和内存管理》
  6. NSError __autoreleasing error 中的 __autoreleasing 修饰符
  7. 黑幕背后的Autorelease
  8. [Objective-C 内存管理——你需要知道的一切]
  9. Autorelease 详解
  10. Autorelease Pool学习笔记

你可能感兴趣的:(ARC 的__autoreleasing相关知识点)