Objective-C 采用的是引用计数式的内存管理方式:

  • 自己生成的对象自己持有。

  • 非自己生成的对象自己也能持有。

  • 自己持有的对象不再需要时释放。

  • 非自己持有的对象自己无法释放。

  • 使用以下名称开头的方法名意味着自己生成的对象只有自己持有

  • alloc

  • new

  • copy

  • mutableCopy

1
2
3
4
5
/*
* 自己生成并持有该对象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
  • 非自己生成的对象,自己也能持有

    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
    4
    id obj = [[NSObject alloc] init];//持有新生成的对象
    [obj doSomething];//使用该对象
    [obj release];//释放该对象,不再持有了
    [obj release];//释放已经废弃了的对象,崩溃

autorelease

当对象超出其作用域时,对象实例的 release 方法就会被调用,autorelease 的具体使用方法如下:

  • 生成并持有 NSAutoreleasePool 对象。
  • 调用已分配对象的 autorelease 方法。
  • 废弃 NSAutoreleasePool 对象。
1
2
3
4
5
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有对象
[obj autorelease]; // 取得的对象存在,但自己不持有该对象
return obj;
}

这个特性是使用 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 的。但是这里有一个疑问,我们常用到的 UITableViewdelegate 属性是这样定义的:@property (nonatomic, assign) id<UITableViewDelegate> delegate;,为什么用的修饰符是assign 而不是 weak?其实这个 assignARC 中意义等同于 __unsafe_unretained(后面会讲到),它是为了在 ARC 特性下兼容 iOS4 及更低版本来实现弱引用机制。一般情况下,你应该尽量使用 weak
  • Block 中防止强引用循环。
  • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (nonatomic, weak) IBOutlet UIButton *testButton;

另外,__weak 修饰符的变量,会被注册到 autoreleasePool 中。

1
2
3
4
{
id __weak obj1 = obj;
NSLog(@"obj2-%@",obj1);
}

编译器转换上述代码如下:

1
2
3
4
5
6
id obj1;
objc_initweak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&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
2
3
4
-  (NSMutableArray *)array  {
NSMutableArray *array = [NSMutableArray array];
return array;
}

转化为

1
2
3
NSMutableArray *array = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(array);
objc_release(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
2
3
4
5
6
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

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

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

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

看一个最常见的例子:

1
2
3
4
5
6
7
8
9
10
11
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 对象在你调用的方法中被创建,然后被放到 AutoreleasePool 中,等到使用结束后随着 AutoreleasePool 的销毁而释放,所以函数外 error 对象的使用者不需要关心它的释放。

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

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

1
2
3
4
5
6
7
8
9
- (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];
}
}];
}
}

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

1
2
3
4
5
6
7
8
9
10
11
- (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 后被释放,正确的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12

- (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;
}
}