浅谈 iOS ARC 内存管理
Objective-C 采用的是引用计数式的内存管理方式:
自己生成的对象自己持有。
非自己生成的对象自己也能持有。
自己持有的对象不再需要时释放。
非自己持有的对象自己无法释放。
使用以下名称开头的方法名意味着自己生成的对象只有自己持有:
alloc
new
copy
mutableCopy
1 | /* |
非自己生成的对象,自己也能持有
1
2
3
4
5/*
* 持有非自己生成的对象
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象备注:通过
retain
方法来让指针变量持有这个新生成的对象。不再需要自己持有的对象时释放
1
2
3
4
5
6
7
8
9/*
* 不在需要自己持有的对象的时候,释放
*/
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/自己持有的对象,一旦不再需要,持有者有义务释放该对象。释放使用
release
方法。
当调用对象的release
方法只是将对象的引用计数器-1
,当对象的引用计数器为0
的时候会调用了对象的dealloc
方法才能进行释放对象的内存。非自己生成的对象持有对象的释放
1
2
3
4
5
6
7
8//非自己生成的对象,暂时没有持有
id obj = [NSMutableArray array];
//通过retain持有对象
[obj retain];
//释放对象
[obj release];
两种不允许的情况:
释放自己不持有的对象
1
2
3
4
5/*
* 非自己持有的对象无法释放
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为释放一个已经废弃了的对象
1
2
3
4id obj = [[NSObject alloc] init];//持有新生成的对象
[obj doSomething];//使用该对象
[obj release];//释放该对象,不再持有了
[obj release];//释放已经废弃了的对象,崩溃
autorelease
当对象超出其作用域时,对象实例的 release
方法就会被调用,autorelease
的具体使用方法如下:
- 生成并持有
NSAutoreleasePool
对象。 - 调用已分配对象的
autorelease
方法。 - 废弃
NSAutoreleasePool
对象。
1 | - (id) getAObjNotRetain { |
这个特性是使用 autorelease
来实现的,autorelease
使得对象在超出生命周期后能正确的被释放(通过调用 release
方法)。在调用 release
后,对象会被立即释放,而调用 autorelease
后,对象不会被立即释放,而是注册到 autoreleasepool
中,当 autoreleasepool
销毁时,会对 autoreleasepool
里面的所有对象做一次 release
操作。
在 ARC
环境下,id
类型和对象类型和 C
语言其他类型不同,类型前必须加上所有权的修饰符。
所有权修饰符总共有4种:
- __strong
- __weak
- __autoreleasing
- __unsafe_unretained
__strong
__strong
表示强引用,对应定义 property
时用到的 strong
。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil
。__strong
修饰符是 id
类型和对象类型默认的所有权修饰符。
__weak
__weak
表示弱引用,对应定义 property
时用到的 weak
。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil
,这样可以防止野指针。__weak
最常见的一个作用就是用来避免强引用循环。
__weak
的几个使用场景:
- 在
delegate
关系中防止强引用循环。在ARC
特性下,通常我们应该设置delegate
属性为weak
的。但是这里有一个疑问,我们常用到的UITableView
的delegate
属性是这样定义的:@property (nonatomic, assign) id<UITableViewDelegate> delegate;
,为什么用的修饰符是assign
而不是weak
?其实这个assign
在ARC
中意义等同于__unsafe_unretained
(后面会讲到),它是为了在ARC
特性下兼容iOS4
及更低版本来实现弱引用机制。一般情况下,你应该尽量使用weak
。 - 在
Block
中防止强引用循环。 - 用来修饰指向由
Interface Builder
创建的控件。比如:@property (nonatomic, weak) IBOutlet UIButton *testButton;
。
另外,__weak
修饰符的变量,会被注册到 autoreleasePool
中。
1 | { |
编译器转换上述代码如下:
1 | id obj1; |
objc_loadWeakRetained
函数获取附有 __weak
修饰符变量所引用的对象并 retain
, objc_autorelease
函数将对象放入 autoreleasePool
中,据此当我们访问 weak
修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用 weak
的话,在我们去访问 weak
修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。
解决方案:
要访问 weak
修饰的变量时,先将其赋给一个 strong
变量,然后进行访问。
为什么访问 weak
修饰的对象就会访问注册到自动释放池的对象呢?
因为 weak
不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在 autoreleasePool
销毁时释放对象占用的内存。
__autoreleasing
在 ARC
模式下,我们不能显示的使用 autorelease
方法了,但是 autorelease
的机制还是有效的,通过将对象赋给 __autoreleasing
修饰的变量就能达到在 MRC
模式下调用对象的 autorelease
方法同样的效果。
__autoreleasing
修饰的对象会被注册到 Autorelease Pool
中,并在 Autorelease Pool
销毁时被释放。
注意:定义 property
时不能使用这个修饰符,因为任何一个对象的 property
都不应该是 autorelease
类型的。
__unsafe_unretained
ARC
是在 iOS5
引入的,而 __unsafe_unretained
这个修饰符主要是为了在 ARC
刚发布时兼容 iOS4
以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义 property
时对应的是 unsafe_unretained
。__unsafe_unretained
修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1
。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil
,所以成为了野指针,非常不安全。
__unsafe_unretained
的应用场景:
- 在 ARC 环境下但是要兼容 iOS4.x 的版本,用
__unsafe_unretained
替代__weak
解决强引用循环的问题。
最后
总结, autorelease
的机制却依然在很多地方默默起着作用,我们来看看这些场景:
- 方法返回值。
- 访问 __weak 修饰的变量。
- id 的指针或对象的指针(id *)。
方法返回值
首先,我们看这个方法:
1 | - (NSMutableArray *)array { |
转化为
1 | NSMutableArray *array = objc_msgSend(NSMutableArray, @selector(array)); |
这里 array
的所有权修饰符是默认的 __strong
。由于 return
使得 array
超出其作用域,它强引用持有的对象本该被释放,但是由于该对象作为函数返回值,所以一般情况下编译器会自动将其注册到 AutoreleasePool
中(注意这里是一般情况下,在一些特定情况下,ARC
机制提出了巧妙的运行时优化方案来跳过 autorelease
机制。)。
ARC 模式下方法返回值跳过 autorelease 机制的优化方案
为什么方法返回值的时候需要用到 autorelease
机制呢?
当对象被作为参数返回 return
之后,如果调用者需要使用就需要强引用它,那么它 retainCount + 1
,用完之后再清理,使它 retainCount - 1
。
如果在方法中创建了对象并作为返回值时,根据 ARC
内存管理的原则,谁创建谁释放。既然作为返回值,就必须保证返回时对象没被释放以便方法外的调用者能拿到有效的对象,否则你返回的是 nil,有何意义呢。所以就需要找一个合理的机制既能延长这个对象的生命周期,又能保证对其释放。这个机制就是 autorelease 机制
。
ARC
模式下在方法 return
的时候,会调用 objc_autoreleaseReturnValue()
方法替代 autorelease
。在调用者强引用方法返回对象的时候,会调用 objc_retainAutoreleasedReturnValue()
方法,该方法会去检查该方法或者调用方的执行命令列表,是否会被传给 objc_retainAutoreleasedReturnValue()
方法。如果里面有 objc_retainAutoreleasedReturnValue()
方法,那么该对象就直接返回给方法或者函数的调用方。达到了即使对象不注册到 autoreleasepool
中,也可以返回拿到相应的对象。如果没传,那么它就会走 autorelease
的过程注册到 autoreleasepool
中。
访问 __weak 修饰的变量
在访问 __weak
修饰的变量时,实际上必定会访问注册到 AutoreleasePool
的对象。如下来年两段代码是相同的效果:
1 | id __weak obj1 = obj0; |
为什么会这样呢?因为 __weak
修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 AutoreleasePool
中,就能保证 AutoreleasePool
被销毁前对象是存在的。
id 的指针或对象的指针(id *)
另一个隐式地使用 __autoreleasing
的例子就是使用 id 的指针或对象的指针(id *) 的时候。
看一个最常见的例子:
1 | NSError *__autoreleasing error; |
error
对象在你调用的方法中被创建,然后被放到 AutoreleasePool
中,等到使用结束后随着 AutoreleasePool
的销毁而释放,所以函数外 error
对象的使用者不需要关心它的释放。
在 ARC
中,所有这种指针的指针类型(id *)
的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing
类型。
有一点特别需要注意的是,某些类的方法会隐式地使用自己的 AutoreleasePool
,在这种时候使用 __autoreleasing
类型要特别小心。比如 NSDictionary
的 enumerateKeysAndObjectsUsingBlock
方法:
1 | - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error { |
上面的代码中其实会隐式地创建一个 AutoreleasePool
,类似于:
1 | - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error { |
为了能够正常的使用 *error
,我们需要一个 strong
类型的临时引用,在 dict
的枚举 Block
中是用这个临时引用,保证引用指向的对象不会在出了 dict
的枚举 Block
后被释放,正确的方式如下:
1 |
|