iOS内存管理—Manual retain-release

翻译自Apple官方文档,是时候表演翻译腔的技术了

介绍

应用内存管理是:在程序运行时,分配内存,使用内存,然后在你使用完成之后释放它。一个编写良好的程序应该尽可能少地使用内存。在Objective-C中,内存管理同样可以被视为:一种在许许多多数据以及代码中,为有限的内存空间分配使用权。当你看完本guide,你就会知道该如何管理app的内存:显式管理对象的生命周期,并且在他们不再被需要的时候释放他们。

虽然内存管理通常被认为是对单个对象而言的,但是你的目标实际上应该是管理对象图(object graph)。你需要确定在内存中的对象不会超过你实际需要的。

graph

概述

Objective-C提供两种途径来管理应用的内存。

  1. 这是本文档阐述的方法,被称为“手动 retain-release”或者MRR,你需要自己追踪对象来显式管理内存。这已经以一种模式实现了,称为“引用计数”,它被Foundation类NSObject集成在运行时环境中。
  2. 另一种方法成为自动引用计数(Automatic Reference Counting, ARC),系统使用跟MRR相同的引用计数体系。但是它是在编译时在适当的地方为你插入内存管理方法的调用。强烈推荐你在新工程中使用ARC。如果你使用ARC,那就没有必要明白这个文档中阐述的底层实现,虽然这在某些情况会很有帮助。想知道更多的关于ARC的内容,查看Transitioning to ARC Release Notes。
良好的练习能为你防止内存有关的问题

有两种问题,会导致错误的内存管理:

  • 对仍在使用的数据进行释放或者重写
    这会导致内存出错,并且通常会引起你的程序崩溃,更坏的是,可能会让用户数据出错。
  • 没有释放不再使用的数据,引起内存泄露
    内存泄露是指被分配出去的内存没有被释放,即使他永远都不会被再次使用。泄露会造成程序使用的内存不断增加,最终会造成系统性能低下或者你的程序被终止。

然而从引用计数的角度考虑内存管理,通常是适得其反的。因为你会倾向于在实现细节层面考虑内存管理,而不是从你的实际目的的角度出发。相反,你应该从对象的拥有关系以及对象图(object graph)中考虑内存管理。

使用分析工具来调试内存问题

为了在编译时找出代码的问题,你可以使用Xcode内置的Clang Static Analyzer。
如果内存管理问题还是出现了,那么你可以使用另外一些手段来分析诊断问题:

内存管理策略

在引用计数环境下使用内存管理,可以通过在NSObject协议中定义的一组发方法,还有“命名惯例”。NSObject类也定义了一个方法,dealloc,当对象被释放的时候自动被调用。这篇文章阐述了所有你在cocoa程序的内存管理中需要知道的基本法则,并且提供了一些关于正确使用的例子。

基本内存管理规则

内存管理模式是基于对象拥有关系的。一个对象可能会有一个或者多个owner。只要一个对象至少拥有一个owner,它就会继续存在。如果一个对象一个owner都没有,那么运行时系统就会自动销毁它。为了确定什么时候你拥有你的对象,而什么时候你没有,cocoa设立了如下的策略:

  • 你拥有你所创建的任何对象
    你可以通过名字以”alloc”、”new”、”copy”、或者”mutableCopy”开头的这些方法来创建对象(例如,alloc,newObject,mutableCopy)。
  • 你可以使用retain来拥有一个对象的所有权
    接收到此调用的对象通常保证在他接收到retain所在的方法中保持有效,并且retain方法会安全地将这个对象返回给调用者。你通常会在这两种情况中使用retain:(1)在accessor方法或者init方法的实现中,取得你想要作为属性值(property value)储存起来的对象的所有权;(2)防止一个对象由于某些操作的副作用而变得不可用。
  • 当你不在需要它,你需要释放你所拥有的对象的所有权
    你可以通过方法releaseautorelease来释放一个对象的所有权。在coco的专有术语中,释放一个对象的所有权通常被称为”releasing”一个对象。
  • 你不能释放一个你并不拥有的对象的所有权
    这是前面的这些政策的必然结果,只是明显地提出来而已。
一个简单的例子

为了将上述的策略描述清楚,考虑下面的代码片段

1
2
3
4
5
6
7
{
Person *aPerson = [[Person alloc] init];
// ...
NSString *name = aPerson.fullName;
// ...
[aPerson release];
}

person对象通过alloc方法来被创建,所以它随后在不再需要它的时候向他发送release消息。person的name(Nsstring *name)并没有通过owning method来被取得所有权,所以它没有被发送release消息。注意,这个例子中使用release而不是autorelease

使用autorelease来发送延迟的release

当你需要发送一个延迟的release消息,你可以使用autorelease——典型的例子是,当你在一个方法中返回一个对象时。例如,你可以实现一个fullName方法,如下:

1
2
3
4
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName] autorelease];
return string;
}

你拥有从alloc中被返回的string。要服从内存管理策略,你就需要在你失去string的引用之前放弃他的所有权。然而如果你使用release,那么string就会在它被返回之前被回收(这个方法就会返回一个不可用的对象)。使用autorelease的话,你就表示你想要放弃所有权,但是你允许方法的调用者在string被回收之前使用它。

你同样可以这样来实现fullName方法

1
2
3
4
- (NSString *)fullName {
NSString *string = [NSString stringQWithFormat:@"%@ %@", self.firstName, self.lastName];
return string;
}

从基本规则来看,你并没有拥有stringWithFormat方法所返回的string的所有权,所以你可以安全地从方法中返回string。

相反地,下面这个实现是错误的

1
2
3
4
- (NSString *)fullName {
NSString *string = [[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName];
return string;
}

根据命名惯例,没有任何东西可以表明fullName的调用者拥有返回的string的所有权。调用者因此没有任何理由来释放返回的string,而它也因此而被泄露了。

你并不拥有通过引用返回的对象

一些在cocoa中的方法阐述了某个对象是通过引用返回的(也就是说,他们接受一个ClassName **或者id *类型的对象)。比较通用的模式是,使用包含错误信息的NSError对象,例如initWithContentsOfURL:options:error: (NSData) and initWithContentsOfFile:encoding:error: (NSString)

在这里情况里面也应用了相同的规则。当你调用了这些方法,你并没有创建NSError对象,所以你并没有拥有它。因而你没有必要去释放他。例子如下:

1
2
3
4
5
6
7
8
9
NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// Deal with error...
}

// ...
[string release];

实现dealloc方法来释放对象的所有权

NSObject对象定义了一个方法,dealloc,当一个对象一个拥有者都没有的话,他就会被自动调用,并且它的内存就会被回收——cocoa术语中它被称为”free”或者”deallocate”。dealloc方法的角色是释放对象自己的内存,并且销毁他所拥有的资源,包括所有对象变量的拥有权。

下面的例子阐述了如何为Person类实现dealloc方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end

@implementations Person
// ...
- (void)dealloc {
[_firstName release];
[_lastName release];
[super dealloc];
}
@end

重要:永远不要直接调用任何对象的dealloc方法。
你需要在你的实现的末尾中调用父类的实现。
你不应该将系统资源的管理与对象生命周期混在一起
当程序终止时,对象可能不会被发送dealloc消息。因为进程的内存在进程退出的时候自动清空,很显然,让操作系统来清空所有资源比逐个逐个调用内存管理方法高效的多。

Core Foundation 使用相似但是不相同的规则

对于Core Foundation对象来说,它们会使用相似的内存管理策略(可查看Memory Management Programming Guide for Core Foundation)。然而对于cocoa与Core Foundation来说,命名惯例是不一样的。特别地,创建Core Foundation的规则并不适用于返回OC对象的方法。例如,在下面的代码片段,你不需要释放myInstance的所有权:

1
MyClass *myInstance = [MyClass createInstance];// ???不明所以然

内存管理实战

虽然上面介绍的基本盖面都很直接明了,但你可以通过一些步骤来让内存的管理更加简单,并且帮助你确保你的程序在减少它所需要的资源时仍然可靠健壮。

使用Accessor方法来让内存管理更加简单

如果你的类拥有一个对象属性,你需要确保所有被设置为属性的对象当你在使用的时候不会被释放掉。因此,当它被设置为属性的时候,你必须要获取这个对象的所有权。你必须同样要确保稍候要释放所有现在正持有的对象的所有权。

可能某种意义上来说这样做很单调枯燥乏味,但是如果你使用使用accessor方法,那么出现内存管理问题的风险就会大大降低。如果你对你的成员变量在普通代码中使用retainrelease,那么你机会注定会出现错误。

举个例子,有一个Counter对象,而你现在要设置它的count

1
2
3
@interface Count: NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

这个属性生命了两个accessor方法。特别地,你应该要求编译器来生成那些方法。然而,看看它们是怎么被实现的也是有好处的。

在“get”方法中,你只是返回一个synthesized的实例变量。所以并不需要retain或者release

1
2
3
- (NSNumber *)count {
return _count;
}

在“set”方法,如果你需要向你的新的object发送retain消息,来获取他的所有权,因而能确保你的对象不会被释放掉。同时你也需要放弃旧的count对象的所有权——向他发送release消息(Objective-C中允许向nil对象发送消息,因而即使_count还没有被设置,下面的实现也依然可以执行)。你需要在[newCount retain]之后调用release,以防newCount与oldCount是同一个对象。否则在[newCount retain]之前,他就被释放掉了。

1
2
3
4
5
6
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];

_count = newCount;
}

使用Accessor方法来设置属性值

假设你想要实现一个方法来重置计数器。你有两个选择,第一个选择是通过alloc来创建NSNumber对象,因此你需要用release来对应。

1
2
3
4
5
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
}

第二种方法使用一个方便的构造器来创建新的NSNumber对象。因此没有必要发送retain或者release对象。

1
2
3
4
- (void)reset {
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
}

注意这两种方法都是用set accesstor方法。

下面这种方法会几乎毫无疑问地对所有简单的情况可用,但是由于他没有使用accessor方法,所以这样做的话会几乎百分百在某些状况引起错误(例如,当你忘记了retain或者release,或者如果内存管理语法更改了)。

1
2
3
4
5
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero;
}

同样需要注意,如果你正在使用kvo,以这种方法来改变变量不符合kvo规则。

不要在initializer方法和dealloc方法中使用accessor方法

唯一你不应该使用accessor方法来设置实例变量的地方是initializer方法以及dealloc。要用一个表示0的number对象来初始化一个counter对象,你可能要像下面这样实现init方法。

1
2
3
4
5
6
7
- (id)init {
self = [super init];
if (self) {
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}

如果要用一个counter对象来初始化另外一个counter对象,而不是0,你需要实现一个initWithCount函数:

1
2
3
4
5
6
7
- (id)initWithCount:(NSNumber *)startingCount {
self = [super init];
if (self) {
_count = [startingCount copy];
}
return self;
}

由于Counter类有一个对象实例变量,你必须实现dealloc方法。他应该释放所有实例变量的所有权,通过给他们发送release消息,并且在最后他应该调用父级的实现:

1
2
3
4
- (void)dealloc {
[_count release];
[super dealloc];
}

使用弱引用来避免retain cycle

retain一个对象会创建一个该对象的强引用(strong reference)。一个对象直到所有指向他的强引用都被释放掉之后才会被释放。有一个被称为retain cycle的问题,会由两个互相引用的对象引起:就是说,他们互相都拥有对方的强引用(更直接点,可以说有一连串对象,上一个拥有下一个对象的强引用,并且最后一个对象拥有第一个对象的强引用)。

下图中展示的对象关系阐述了一种可能会发生的retain cycle。document对象拥有一个page对象,代表文档中的每一页。每一个page对象拥有一个属性来追踪他是在哪一个文档中的。如果document对象对page拥有一个强引用,并且page对象同样也对document对象拥有一个强引用,那么他们任一个都不会被释放。document的引用在page对象被释放之前不会被置零,并且在document对象被释放之前page对象不会被释放。

feature

解决retain cycle的办法是使用弱引用(weak reference)。一个弱引用是一种“不拥有”的关系。对象a拥有对象b的弱关系,但实际上a并没有retain b。

然而要保证object graph是完整的,那么就应该在某处有一些强引用(如果只有弱引用,page以及paragraph会因为没有任何owner而被释放掉)。cocoa因而建立了一种惯例,“父对象”应该持有对他的“孩子”的强引用,并且孩子应该持有其父亲的弱引用。

因此,上述的例子中,document对象拥有一个强引用来指向他的page对象,而page对象拥有指向document对象的弱引用。

cocoa中的弱引用包含但不仅限于:table data sources,outline view items,notification observers,还有miscellaneous targets和delegates。

在你需要向持有弱引用的对象发送消息的时候,需要留神。如果你向一个被释放掉的对象发送消息,你的程序会崩溃。你需要非常清楚你的对象什么时候有效什么时候变得不可用。在大多数情况,被弱引用指向的对象在被回收前有责任通知其他对象。例如,当年你在notification center注册了一个对象,notification center保存那个对象的弱引用,并且当合适的通知被发送时,通知中心会向该对象发送消息。当对象被回收时,你需要将它从notification center中取消注册,好让消息中心不再向该对象发送消息。同样地,当delegate对象被回收,你需要将delegate移除,通过setDelegate函数将他设置为nil。这些调用通常都会被放在dealloc函数中。

避免引起对你正在使用的对象的deallocation

cocoa的所有权政策指定了,接受消息的对象需要在整个函数作用域之内可用。同样也应该保证,能够在当前被调用的方法中返回这个接收消息的对象,而不用担心它被释放掉。对你的程序来说,一个对象的getter方法返回一个对象还是一个算数值并不重要,重要的是那个返回的对象在你需要的时候仍然可用。

下面是一些在这个规则之下,偶尔会出现的exception,主要是两类之一:

1. 当一个对象从基本容器类中被移走

1
2
3
heisenObject = [array objectAtIndex: n];
[array removeObjectAtIndex: n];
//heisen 对象现在可能会不可用

当一个对象从基础容器类中被移除时,它会被发送release消息,而不是autorelease消息。如果那个容器是被移除对象的唯一拥有者,那么被移除的对象(例如heisenObject)就会被马上dealloc。

2. 当“父对象”被释放

1
2
3
4
5
id parent = <#create a parent object#>;
//...
heisenObject = [parent child];
[parent release];//或者,例如: self.parent = nil;
//heisenObject 现在可能会不可用

在某些情况中,你从其他对象接受了一个对象A,然后直接或者间接release了父对象。如果release父对象会造成它被dealloc,并且那个父对象是对象A的唯一拥有者,那么对象A(就像heisenObject)就会同时被deallocate(假设它在父对象的dealloc方法中被发送release消息而不是autorelease消息)。

为了避免这些情况,你需要在接受到heisenObject的时候retain,并且当你使用完的时候释放它。例如:

1
2
3
4
heisenObject = [[array objectAtIndex: n] retain];
[array removeObjectAtIndex: n];
//Use heisenObject...
[heisenObject release];

不要使用dealloc来管理稀缺资源

你不应该在dealloc方法中管理稀缺资源,例如file descriptor, network connection, and buffer or caches。特别地,你不应该试图设计出一个能主动调用dealloc函数的类。dealloc的调用可能会由于bug或者程序挂掉而延时或者被跳过。

相反,如果你有一个管理稀缺资源的类,你应该这样来设计你的程序:当你不再需要使用某个稀缺资源的时候,可以告诉管理资源的类对象,来让它执行“clean up”操作。然会你会对那个对象发送release消息,接着它会被dealloc,但是就算它没有被dealloc,你也不会遭受额外问题。

但是如果你试图将资源管理放在dealloc中,就会出问题。例如:
1. 对象图释放时的顺序依赖
对象图释放机制是无序的。即使你可能会预想——或者得到——一个特定的准许,但是你同时也会引入脆弱性。如果一个对象意外地被autorelease而不是release,那么释放的顺序就会改变,而这可能会导致意外结果。

2. 稀缺资源的不回收
内存泄露是应该被修复的bug,但是他们通常不会立即致命。如果稀缺资源在你希望他们被release的时候没有被释放,那么你会引起更加严重的问题。例如如果你的程序将descriptors耗尽,那么用户可能无法保存数据。

3. 清理逻辑在错误的线程里面被执行
如果一个对象在意外的时间点被autorelease,而且它恰好进入了某个线程的autorelease pool,那么它就会被dealloc。对那些需要在其他线程里面被访问的资源来说,这是很致命的。

容器拥有它们包含的对象

当你将一个对象添加到容器中(例如一个array, dictionary或者set),这些容器就会拥有它的所有权。当这个对象从容器中被移除,或者容器本身被释放掉,容器会释放对其内对象的所有权。因此,几个例子,如果你想要创建numbers的数组,你可以选择一下任一种方法:

1
2
3
4
5
6
7
NSMutableArray *array = <#Get a mutable array#>;
NSUinteger i;
//...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject: convenienceNumber];
}

在这个例子中,你没有调用alloc,所以你也不需要调用release。而且也没有必要retain新生成的number,因为array会去做。

1
2
3
4
5
6
7
8
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
//...
for (i = 0; i < 10; i++) {
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger: i];
[array addObject:allocedNumber];
[allocedNumber release];
}

在这个例子中,你需要向number发送release消息,因为你调用了alloc。由于数组会retain添加到其中的number,所以只要它还在数组中,它就不会被释放。

想要明白这,你想象正在实现一个容器类。你需要确定你接受的对象不会在你眼皮底下消失,所以你需要在它们被传进来的时候向它们发送retain消息。如果他们被移走,你需要发送release来平衡。并且你需要在自己的dealloc方法中向所有剩下的对象发送release

“拥有关系”策略使用引用retain count来实现的

ownership策略通过引用计数来实现——通常被称为“retain count”。每一个对象都有retain count。

  • 当你创建一个对象,它有一个值为1的retain count
  • 当你向一个对象发送retain消息,它的retain count会加1
  • 当你向一个对象发送release消息,它的retain count会减1;当你向一个对象发送autorelease消息,它的retain count会在当前的autorelease pool块的末尾减1
  • 如果一个对象的retain count减为0,它会被释放。

重要:你不应该显示查询某个对象的retain count是多少(详见retainCount)。因为得到的结果总是不准确的,你可能不知道除了你以外有多少framework对象会retain你的对象。在调试内存问题的时候,你应该关心的是:确保你的代码遵守了ownership规则。

使用Autorelease Pool Blocks

Autorelease pool block提供了一种机制,让你可以释放对对象的所有权,但是避免了对象被马上释放的可能性(例如当你在某个方法中返回对象)。特别地,虽然你不需要创建自己的Autorelease pool block,但是有一些情况你还是最好这样做。

关于Autorelease Pool Block

一个autorelease pool block使用@autoreleasepool来标识,如下:

1
2
3
@autoreleasepool {
// 创建autorelease 对象
}

在autorelease pool block的末尾,在pool内被发送autorelease消息的对象会被发送release消息。

就像其他代码块,autorelease pool block也可以被嵌套:

1
2
3
4
5
6
@autorealsepool {
//...
@autoreleasepool {
//...
}
}

实际上你一般不会看到像上面那样的代码,典型的情况是,在某个文件中autorelease pool block里的代码会调用在另外一个文件中被包含在其他autorelease pool block中的代码。对于一个给定的autorelease消息,相应的release消息会在autorelase被发送所在的autorelease pool block 末尾被发送。

cocoa 通常都会期望代码在autorelease pool block中被执行,否则autorelease对象不会被释放而你的程序因此而泄露内存。(如果你在autorelease pool block之外向对象发送autorelease消息,cocoa会log一个恰当的错误信息)。AppKit跟UIKit框架都会在autorelease pool block中来进行某些时间循环迭代(例如鼠标点击或者点击)。因此通常不需要自己创建autorelease pool block,甚至很少看到创建的代码。然而,有三种情况,你可能需要使用自己的autorelease pool block:

  • 如果你正在编写不是基于UI framework的程序,例如command-line tool。
  • 如果你编写一个会创建很多临时对象的循环。
    你可能会在循环中使用一个autorelease pool block来在下一次迭代之前销毁那些对象。在循环中使用一个autorelease pool block能够帮助降低程序占用的最大内存。
  • 如果你生成一个子线程
    你需要在线程开始被执行的时候创建自己的autorelease pool block,否则,你的程序会泄露对象。

使用Autorelease Pool Block来降低内存峰值

很多程序会创建autoreleased临时对象。这些对象在到达pool末尾之前,都会增加程序的内存占用值。很多情况中,允许临时对象累积在当前循环中累积不会引起过分的开销;然而在另外一些情况中,你可能会创建大量的临时对象,并且让内存的占用飙升不少,因而你需要尽快将他们释放。在这些少数情况里面,你可以创建自己的autorelease pool block。在block的末尾,那些临时对象会被释放掉,因而它们能够被释放,而降低程序的内存占用。

下面的例子示范了如何为循环使用局部autorelease pool block:

1
2
3
4
5
6
7
8
NSArray *urls = <# an array of file urls #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringwithContentsOfURL:url encoding: NSUTF8StringEncoding error:&error];
/*对字符串进行处理,创建并且自动释放更多对象。*/
}
}

佛如循环一次处理一个文件。任何在autorelease pool block中被发送autorelease消息的对象都会在pool结束的时候被释放。

在autorelease pool block后面,你应该将所有在autorelease pool block中被发送autorelease消息对象当作已经被销毁。不要试图再向它们发送消息或者在你的方法中向调用者返回它们。如果你必须在一个autorelease pool block之外使用一个临时的对象,就下下面例子中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id)findMatchingObject:(id)anObject {
id match;
while (match == nil) {
@autoreleasepool {
/*执行一个会产生大量临时对象的查找操作*/
match = [self expensiveSearchForObject: anObject];

if (match != nil) {
[match retain];/* 保存match的所有权 */
}
}
}

return [match autorelease]; /*返回它*/
}

在autorelease pool block中向match发送retain,能够将它的生命周期延展到pool之外,让它能够在循环之外接受消息并且能够被返回到findMatchingObject的调用者中。

Autorelease Pool Block 与线程

在cocoa 程序中,每个线程都会维持它自己的autorelease pool block栈。如果你正在编写一个foundation-only的程序,或者如果你生成自己的子线程,那么你就需要创建自己的autorelease pool block。

如果你的程序或者线程是长时间存在的或者可能生成大量的autoreleased对象,你应该使用autorelease pool blocks(就像AppKit和UIKit在主线程做的那样)。否则,autoreleased对象会累积然后你程序使用的内存会一直增长。如果你生成的子线程不会make coca call,你不需要使用autorelease pool block。

注意:如果你使用POSIX线程API创建子线程而不是使用NSThread,那么你不能使用cocoa除非cocoa是处于多线程模式。cocoa只有在穿件了它的第一个NSThread对象之后才能进入多线程模式。想要在二级POSIX线程中使用cocoa,你的程序必须要首先生成最少一个NSThread对象,which can immediately exit。你可以使用NSThread的类方法isMultiThreaded来测试cocoa是否处于多线程模式。

(完,未校对)



以上