引子
apple 的《Transitioning to ARC Release Notes》一文里,其中一小节"ARC Introduces New Lifetime Qualifiers"(ARC 引入新的生命周期限定符)中对 Variable Qualifiers(变量限定符)的说明,有一段代码引起了我的注意:
翻译过来是:
你也要注意通过引用方式传入的对象。以下代码会正常运转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。
问题来了:
- -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;中为什么需要用 __autoreleasing 变量限定符修饰?
- 本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量?
要解决上述两个问题,首先得知道
- __autoreleasing是什么?
- __autoreleasing作用什么?(为什么要使用__autoreleasing)
- __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强制执行新规则);
小节上可知:
不能显示的调用dealloc,实现或调用 retain, release, retainCount,或 autorelease。
-
不能使用 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 }
通过上述的对比可知:
-
可通过 @autoreleaepool块 来替代 NSNSAutoreleasePool类对象的生成、持有、以及废弃;
1. ARC 的第1行代码 与 MRC 的第1行代码等价 2. ARC 的第2行代码 与 MRC 的第2和3行代码等价 3. ARC 的第3行代码 与 MRC 的第4行代码等价
通过将对象赋值给带有 __autoreleasing 修饰符的变量来代替调用 autorelease 方法,即将对象注册到 autoreleasepool;
因为不能使用 NSAutoreleasePool 和 调用 autorelease方法了,所以在给出一个对应的替换方案
使用@autoreleaepool块
对应 使用NSAutoreleasePool
;
使用__autoreleasing
修饰符对应 调用autorelease
方法
但是实际情况是:通常情况下不会显式地使用 __autoreleasing修饰符,就像不会显式地使用 __strong修饰符一样;
如下就是不显式使用__autoreleasing
但对象同样被注册到 pool的3个例子
- 编译器会检查方法名是否以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;
- 虽然__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 被销毁之前都能确保该对象存在。
- 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;
}
对比之前提到的内存管理原则:
- 方法
- (BOOL)performOperationWithError:(NSError *__autoreleasing *)error;
不以alloc/new/copy/mutableCopy开头的; - 声明为
NSError *__autoreleasing *
类型的 error 作为*error
被赋值;
以上两点都符合“非自己生成并持有对象”规则,所以 error 取得是:注册到 autoreleasepool,并取得非自己生成并持有的对象;
三、__autoreleasing使用注意点有哪些?
- 对象指针赋值时,所有权修饰符必须一致
//编译错误
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; //此处为对象赋值
- (参照以上3种不显式使用
__autoreleasing
,对象同样被注册到 autoreleasepool 的3个例子)虽然可以非显式使用__autoreleasing修饰符,但在显式地指定__autoreleasing修饰符时,必须要注意对象变量只能为自动变量(包括:局部变量、函数以及方法参数)
Ref
apple doc:
- Using Autorelease Pool Blocks
- Transitioning to ARC Release Notes
other doc:
- Objective-C 中的内存分配
- 文档翻译-Using Autorelease Pool Blocks
- 理解AutoRelease Pool
- 迁移至ARC版本说明(Transitioning to ARC Release Notes)
- 《Obejctive-C 高级编程 iOS 与 OS X 多线程和内存管理》
- NSError __autoreleasing error 中的 __autoreleasing 修饰符
- 黑幕背后的Autorelease
- [Objective-C 内存管理——你需要知道的一切]
- Autorelease 详解
- Autorelease Pool学习笔记