iOS内存管理

引用计数

每个对象都有一个引用计数(retainCount),对对象obj执行[obj retain],其引用计数就+1;执行[obj release],其引用计数就-1。当对象的引用计数减至0时,就会被销毁。引用计数是一种十分简单的内存管理方式,但是如果针对每个对象都要手写retainrelease操作会相当麻烦,而且很容易产生错误。

内存管理的思考方式

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

在内存管理中有个约定,使用以前名称开头的方法,意味着自己生成并持有对象:

  • alloc
  • new
  • copy
  • mutableCopy

使用这类方法生成的对象需要对应的 release 方法来释放。但是,其他类方法也可以生成对象,但并不持有对象,也就不用再 release,如[NSMutableArray array]。其实这个方法内部调用了autorelease,因此不用再手动 release 了。

简单来说,生成并持有对象的方法需要有一个 release 方法与之对应。每个 retain 方法需要一个 release 方法与之对应。只要遵循4条内存管理的思考方式就可以了!

自动引用计数(ARC)

ARC 是苹果从iOS5开始引入的新技术,有了 ARC,就无需再次键入 retain 或者是 release 代码了。

ARC 的官方解释:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects. Rather than having to think about retain and release operations, ARC allows you to concentrate on the interesting code, the object graphs, and the relationships between objects in your application.

ARC 只对 Objective-C 对象有效,所以对于 Core Fundation 对象则需要手动管理内存。关于这两种对象如何转换,可参考另一篇

ARC 所有权修饰符

__strong

__strong 修饰符是对象的默认修饰符。

1
id obj = [[NSObject alloc] init];

obj 其实是一个强类型指针。上面的代码在非 ARC 环境下的代码也是一样的。但是在 ARC 下,当 obj 离开作用域之前就会自动调用[obj release]

也就是说,附有__strong 修饰符的变量在超出其变量作用域时,会释放其被赋予的对象。可以理解为,__strong 修饰的变量会自动持有对象,只要对象被持有,就不会被释放。当变量离开作用域时就会失效,如果对象失去了所有持有者,就会被废弃。

同样,在方法参数上,默认附有__strong 修饰符。

__weak

__weak 修饰的变量不会持有对象。当对象被废弃,变量会自动置为 nil。

__unsafe_unretained

unsafe_unretained 修饰符与 weak 类似,但是对象废弃时,变量不会自动变成 nil。因此变量可能只想无用的内存,成为“悬垂指针”,访问它就会导致程序崩溃。

但是在 iOS4 的程序中,需要用 unsafe_unretained
代替
weak。

__autoreleasing

对象赋值给附有 __autoreleasing 修饰符的变量等价于在 ARC 无效时调用对象的 autorelease 方法,即对象被注册到 autoreleasepool。

__autoreleasing 很少显示使用。因为有以下几种情况:

  1. 类方法创建的对象会自动注册到 autoreleasepool
    1
    id obj = [NSMutableArray array];

如上代码,编译器会检查方法名,不是创建并持有对象的方法,则自动将返回的对象注册到 autoreleasepool。(可见苹果中命名的重要性!)

  1. 访问 __weak 修饰的变量(不是很理解)
    1
    2
    id __weak obj1 = obj0;
    NSLog(@"class=%@", [obj1 class]);

以下源代码与此相同:

1
2
3
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

因为在访问 obj1 的过程中,该对象有可能被废弃。如果把要访问的对象注册到 autoreleasepool 中,那么在 @autoreleasepool 块结束之前都能确保该对象存在。

  1. 指针的修饰符
    例如 Cocoa 框架中很常用的传递 NSError 对象的指针:
1
2
3
4
5
6
7
- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error
{
/* 错误发生 */
*error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
return NO;
}

参数声明会自动变成上面这样。因为作为 alloc/new/copy/mutableCopy 方法返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己生成并持有的对象,都需要注册到 autoreleasepool。

外部调用的代码会变成:

1
2
3
4
NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
BOOL result = [obj performOperationWithError:&tmp];
error = tmp;

ARC 的规则

  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不要显式调用 dealloc
  • 使用 @autoreleasepool 块代替 NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为 C 语言结构体(struct/union)的成员
  • 显示转换“id”和“void *”

ARC 的实现方式

__strong

粗浅地说,ARC 是通过在编译时分析对象的声明周期,并插入 C 接口实现的 retain/release/autorelease 代码。

细节上要复杂一些,详见《Objective-C 高级编程》。

__weak

在废弃对象时,将废弃对象的地址作为键值检索 weak 表,将所有对应的附有 __weak 修饰符的变量设置为 nil。

一些优化

alloc/new/copy/mutableCopy 方法以外的类方法在返回创建的对象时,会检查函数调用方。如果调用方将持有返回对象,就不将对象注册到 autoreleasepool 中而直接返回。

对于连续成对出现的 retain、release,ARC 也会将额外的操作省略。

autorelease

autorelease 类似于 C 语言中自动变量(局部变量)的特性。

调用 autorelease 的对象将被添加到 autoreleasepool 中,到 autoreleasepool 销毁时,其中所有的对象都会调用 release 方法。反之,只要不废弃 NSAutoreleasePool 对象,那么生成的对象就不能被释放。因此,在读入大量数据时,可能会产生内存不足的现象,可以通过手动管理 autoreleasepool 对象来解决。

思考

ARC 有很多优点:

  • ARC 相比手动管理内存更加高效、安全
  • 没有运行时的额外开销,保证了运行的效率
  • 应用 ARC 的代码和非 ARC 代码编译之后是完全一致的,因此可以很好的兼容,将非 ARC 工程向 ARC 过渡

但是,ARC 有引用计数式内存管理的问题,即循环引用问题。这个问题可以通过 weak 解决,需要人为处理一下。此外,ARC 只对 Objective-C 对象有效,Core Fundation 对象还是需要手动管理内存。

与 Java 的垃圾回收(GC)相比:

  • GC 是针对运行时的,而 ARC 是编译时的(ARC 运行效率更高)
  • GC 通过检查可达性标记需要回收的对象,而 ARC 通过分析对象生命周期插入内存管理语句。因此 ARC 存在循环引用的问题

ARC 与 GC 更多的比较请看这里

参考资料