应用程序内存管理是在程序运行时分配内存,使用它并在完成后释放内存的过程。编写良好的程序使用尽量少的内存。在 Objective-C 中,它还能够被看做是在许多数据和代码之间分配有限内存资源的全部权的一种方式。内存管理的核心思想是:明确管理对象的生命周期,并在再也不须要时释放它们。数组
虽然内存管理一般被视为单个对象的级别,但你的目标其实是管理对象图。你但愿确保内存中没有比实际须要的对象更多的对象。缓存
Objective-C 提供了两种应用程序内存管理方法。安全
“manual retain-release” 或 MRR,你经过跟踪你拥有的对象来明确管理内存。这是使用一个称为引用计数的模型实现的,Foundation 框架中的 NSObject 类与运行时环境一块儿提供。网络
在自动引用计数或 ARC 中,系统使用与 MRR 相同的引用计数系统,但它在编译时为你插入适当的内存管理方法调用。若是你使用 ARC,一般不须要理解底层实现,尽管在某些状况下它可能会有所帮助。框架
这里有两种主要致使内存管理不正确的问题:工具
释放或覆盖仍在使用的数据性能
这会致使内存损坏,而且一般会致使应用程序崩溃,甚至致使用户数据损坏。ui
不释放再也不使用的数据会致使内存泄漏atom
内存泄漏是指未释放已分配内存,即便它从未再次使用过。泄漏会致使应用程序不断增长的内存使用量,从而致使系统性能降低或应用程序被终止。url
可是,从引用计数的角度考虑内存管理一般会拔苗助长,由于你倾向于根据实现细节而不是实际目标来考虑内存管理。相反,你应该从对象全部权和对象图的角度考虑内存管理。
Cocoa 使用简单的命名约定来指示你拥有方法返回的对象的时间。
虽然基本策略很简单,但你能够采起一些实际步骤来简化内存管理,并帮助确保你的程序保持可靠和健壮,同时最大限度地减小其资源需求。
自动释放池提供了一种机制,你能够经过该机制向对象发送“延迟(deferred)”释放消息。这在你想要放弃对象的全部权但但愿避免当即释放它的可能性(例如从方法返回对象时)的状况下很是有用。有时你可能会使用本身的自动释放池。
要在编译时识别代码问题,可使用 Xcode 中内置的 Clang Static Analyzer。
若是仍然出现内存管理问题,你可使用其余工具和技术来识别和诊断问题。
许多工具和技术都在 Technical Note TN2239,iOS Debugging Magic 中进行了描述,特别是使用 NSZombie 来帮助找到过分释放的对象。
你可使用 Instruments 跟踪引用计数事件并查找内存泄漏。
在引用计数环境中用于内存管理的基本模型由 NSObject 协议中定义的方法和标准方法命名约定的组合提供。NSObject 类还定义了一个方法 dealloc,该方法在对象销毁时自动调用。
内存管理模型基于对象全部权。任何对象均可能拥有一个或多个全部者。只要一个对象至少有一个全部者,它就会继续存在。若是对象没有全部者,则运行时系统会自动销毁它。为了确保什么时候你拥有对象什么时候你不拥有对象的时机是清晰的,Cocoa 设置如下策略:
你拥有本身建立的任何对象
使用名称以“alloc”,“new”,“copy”或“mutableCopy”开头的方法(例如,alloc,newObject 或 mutableCopy)建立对象。
你可使用 retain 获取对象的全部权
一般保证接收到的对象在接收到的方法中保持有效,而且该方法也能够安全地将对象返回给其调用者。在两种状况下使用 retain:(1)在 accessor 方法或 init 方法的实现中,获取要存储为对象属性的对象的全部权;(2)防止对象因某些其余操做的反作用而失效。
当你再也不须要它时,你必须放弃你拥有的对象的全部权
你经过向对象发送 release 消息或 autorelease 消息来放弃对象的全部权。所以,在 Cocoa 术语中,放弃对象的全部权一般被称为“释放(releasing)”对象。
你不得放弃你不拥有的对象的全部权
这只是先前明确规定的策略规则的必然结果。
要说明策略,请考虑如下代码片断:
{
Person *aPerson = [[Person alloc] init];
// ...
NSString *name = aPerson.fullName;
// ...
[aPerson release];
}
复制代码
Person 对象是使用 alloc 方法建立的,所以在再也不须要时会发送一条释放消息。不使用任何拥有方法检索此人的姓名,所以不会发送 release 消息。但请注意,该示例使用的是 release 而不是 autorelease。
当你须要发送延迟 release 消息时,一般在从方法返回对象时使用 autorelease。例如,你能够像这样实现 fullName 方法:
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName] autorelease];
return string;
}
复制代码
你拥有 alloc 返回的字符串。要遵照内存管理规则,你必须在丢失对该字符串的引用以前放弃该字符串的全部权。可是,若是使用 release,则在返回以前将释放该字符串(而且该方法将返回无效对象)。使用 autorelease,表示你要放弃全部权,但容许方法的调用者在释放以前使用返回的字符串。
你还能够像这样实现 fullName 方法:
- (NSString *)fullName {
NSString *string = [NSString stringWithFormat:@"%@ %@",
self.firstName, self.lastName];
return string;
}
复制代码
遵循基本规则,你不拥有 stringWithFormat: 返回的字符串,所以你能够安全地从方法返回字符串。
相比之下,如下实现是错误的:
- (NSString *)fullName {
NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName];
return string;
}
复制代码
根据命名约定,没有任何东西能够表示 fullName 方法的调用者拥有返回的字符串。所以调用者没有理由释放返回的字符串,所以它将被泄露。
Cocoa 中的一些方法指定经过引用返回一个对象(即,它们采用 ClassName ** 或 id * 类型的参数)。常见的模式是使用 NSError 对象,该对象包含有关错误的信息(若是发生),如 initWithContentsOfURL:options:error: (NSData) 和 initWithContentsOfFile:encoding:error: (NSString) 所示。
在这些状况下,适用的规则与已经描述的相同。当你调用这些方法中的任何一个时,你不会建立 NSError 对象,所以你不拥有它。所以无需释放它,以下例所示:
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];
复制代码
NSObject 类定义了一个方法 dealloc,该方法在对象没有全部者而且其内存被回收时自动调用 - 在 Cocoa 术语中它被“释放(freed)”或“解除分配(deallocated)”。dealloc 方法的做用是释放对象本身的内存,并释放它拥有的任何资源,包括任何对象实例变量的全部权。
@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
@implementation Person
// ...
- (void)dealloc
[_firstName release];
[_lastName release];
[super dealloc];
}
@end
复制代码
重要说明:永远不要直接调用另外一个对象的 dealloc 方法。你必须在实现结束时调用超类的实现。你不该该将系统资源的管理与对象生命周期联系起来。当应用程序终止时,可能不会向对象发送 dealloc 消息。由于进程的内存在退出时自动清除,因此仅仅容许操做系统清理资源比调用全部内存管理方法更有效。
Core Foundation 对象有相似的内存管理规则。可是,Cocoa 和 Core Foundation 的命名约定是不一样的。特别是,Core Foundation 的 Create Rule 不适用于返回 Objective-C 对象的方法。例如,在如下代码片断中,你不负责放弃 myInstance 的全部权:
MyClass *myInstance = [MyClass createInstance];
复制代码
虽然内存管理策略中描述的基本概念很简单,但你能够采起一些实际步骤来简化内存管理,并帮助确保你的程序保持可靠和健壮,同时最大限度地减小其资源需求。
若是你的类具备做为对象的属性,则必须确保在你使用它时不会释听任何设置为该值的对象。所以,你必须在设置对象时声明对象的全部权。你还必须确保放弃任何当前持有的值的全部权。
有时它可能看起来很乏味或迂腐,但若是你一直使用访问器方法,那么内存管理问题的可能性会大大下降。若是你在整个代码中使用实例变量的 retain 和 release,那么你几乎确定会作错事。
考虑一个你想要设置其计数的 Counter 对象。
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
复制代码
该属性声明了两种访问方法。一般,你应该要求编译器合成方法; 可是,了解它们如何实现是有益的。
在“get”访问器中,你只需返回合成的实例变量,所以不须要 retain 或 release:
- (NSNumber *)count {
return _count;
}
复制代码
在“set”方法中,若是其余全部人都按照相同的规则进行游戏,则必须假设新计数能够随时处理,所以你必须取得对象的全部权 - 经过向其发送 retain 消息 - 以确保它不会被释放。你还必须经过向其发送 release 消息来放弃旧计数对象的全部权。(在 Objective-C 中容许向 nil 发送消息,所以若是还没有设置 _count,则实现仍然有效。)你必须在[newCount retain]以后发送此消息,以防二者是同一个对象 - 你不想无心中致使它被释放。
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
复制代码
假设你要实现重置计数器的方法。你有几个选择。第一个实现使用 alloc 建立 NSNumber 实例,所以你能够经过 release 将其进行平衡。
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
}
复制代码
第二个使用便捷构造方法来建立新的 NSNumber 对象。所以,不须要 retain 或 release 消息
- (void)reset {
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
}
复制代码
请注意,二者都使用 set 访问器方法。
对于简单的状况,如下几乎确定会正常工做,可是尽量避免使用访问器方法,这样作几乎确定会在某个阶段致使错误(例如,当你忘记 retain 或 release 时,或者若是实例变量的内存管理语义更改)。
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero;
}
复制代码
另请注意,若是使用 key-value observing,则以这种方式更改变量不会触发 KVO。
你不该该使用访问器方法来设置实例变量的惟一地方是初始化方法和 dealloc。要使用表示零的数字对象初始化计数器对象,能够按以下方式实现 init 方法:
- init {
self = [super init];
if (self) {
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
复制代码
要容许使用非零计数初始化计数器,你能够实现 initWithCount: 方法,以下所示:
- initWithCount:(NSNumber *)startingCount {
self = [super init];
if (self) {
_count = [startingCount copy];
}
return self;
}
复制代码
因为 Counter 类具备对象实例变量,所以还必须实现 dealloc 方法。它应该经过向它们发送 release 消息来放弃任何实例变量的全部权,并最终应该调用 super 的实现:
- (void)dealloc {
[_count release];
[super dealloc];
}
复制代码
Retaining 对象会建立对该对象的强引用。在 released 全部强引用以前,不能释放对象。所以,若是两个对象可能具备循环引用,则会出现一个被称为循环 retain 的问题 - 也就是说,它们彼此之间具备强引用(直接或经过一系列其余对象,每一个对象都强引用下一个对象直到回到第一个)。
图1中所示的对象关系说明了潜在的循环 retain。Document 对象具备文档中每一个页面的 Page 对象。每一个 Page 对象都有一个属性,用于跟踪它所在的文档。若是 Document 对象具备对 Page 对象的强引用,而且 Page 对象具备对 Document 对象的强引用,则任何对象都不能被释放。在释放 Page 对象以前,Document 的引用计数不能为零,而且在取消分配 Document 对象以前不会释放 Page 对象。
图 1 循环引用的图示
循环 retain 问题的解决方案是使用弱引用。弱引用是非拥有关系,其中源对象不保留它具备引用的对象。
可是,为了保持对象图无缺无损,必须在某处有强引用(若是只有弱引用,则页面和段落可能没有任何全部者,所以将被释放)。所以,Cocoa 创建了一个约定,即父对象应该对其孩子保持强引用,而且孩子们应该有对父对象的弱引用。
所以,在图1中,文档对象具备对(retains)其页面对象的强引用,但页面对象具备对(not retain)文档对象的弱引用。
Cocoa 中弱引用的示例包括但不限于 table data sources,outline view items,notification observers 以及其余 targets 和 delegates。
你须要注意将消息发送到仅包含弱引用的对象。若是在销毁对象后向对象发送消息,则应用程序将崩溃。对象有效时,你必须具备明肯定义的条件。在大多数状况下,弱引用对象知道另外一个对象对它的弱引用,就像循环引用的状况同样,而且负责在销毁时时通知另外一个对象。例如,当你向通知中心注册对象时,通知中心会存储对该对象的弱引用,并在发布相应的通知时向其发送消息。销毁对象后,你须要将其注销到通知中心,以防止通知中心向该对象发送任何再也不存在的消息。一样,当销毁 delegate 对象时,你须要经过向另外一个对象发送带有 nil 参数的 setDelegate: 消息来删除 delegate 引用。这些消息一般从对象的 dealloc 方法发送。
Cocoa 的全部权策略指定接收的对象一般应该在调用方法的整个范围内保持有效。还应该能够从当前范围返回接收到的对象而不用担忧它被释放。对于你的应用程序而言,对象的 getter 方法返回缓存的实例变量或计算值应该可有可无。重要的是该对象在你须要的时间内仍然有效。
此规则偶尔会有例外状况,主要分为两类。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
复制代码
从其中一个基本集合类中删除对象时,会向其发送一个 release(而不是 autorelease)消息。若是集合是已删除对象的惟一全部者,则会当即释放已删除的对象(示例中为 heisenObject)。
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
复制代码
在某些状况下,你从另外一个对象检索对象,而后直接或间接释放父对象。若是释放父对象致使它被释放,而且父对象是子对象的惟一全部者,则子对象(示例中的 heisenObject)将同时被释放(假设它被发送一个 release 而不是一个 autorelease 消息在父对象的 dealloc 方法中)。
为了防止这些状况,你在收到 heisenObject 后会保留它,并在完成后将其释放。例如:
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
复制代码
你一般不该该在 dealloc 方法中管理稀缺资源,例如文件描述符,网络链接以及缓冲区或缓存。特别是,你不该该设计类,以便在你认为将调用 dealloc 时调用 dealloc。因为错误或应用程序拆除(tear-down),dealloc 的调用可能会被延迟或回避。
相反,若是你有一个实例管理稀缺资源的类,你应该设计你的应用程序,以便你知道什么时候再也不须要资源,而后能够告诉实例在那时“清理”。你一般会释放该实例,dealloc 会跟随,但若是没有,你将不会遇到其余问题。
若是你尝试在 dealloc 之上捎带资源管理,则可能会出现问题。例如:
对象图拆除的顺序依赖性。
对象图拆除机制本质上是无序的。虽然你一般会指望并得到特定顺序,但你会引入脆弱性。若是对象意外地自动释放而不是正常释放,则拆卸顺序可能会改变,这可能会致使意外结果。
不回收稀缺资源。
内存泄漏是应该修复的错误,但它们一般不会当即致命。可是,若是在你但愿释放资源时没有释放稀缺资源,则可能会遇到更严重的问题。例如,若是你的应用程序用完了文件描述符,则用户可能没法保存数据。
在错误的线程上执行清理逻辑。
若是一个对象在乎外的时间自动释放,它将在它碰巧进入的任何线程的自动释放池块中被释放。对于只能从一个线程触及的资源来讲,这很容易致命。
将对象添加到集合(例如数组,字典或集合)时,集合将得到对象的全部权。当从集合中移除对象或集合自己被释放时,集合将放弃全部权。所以,例如,若是要建立数字数组,能够执行如下任一操做:
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject:convenienceNumber];
}
复制代码
在这种状况下,你没有调用 alloc,所以无需调用 release。不须要保留新数字(convenienceNumber),由于数组会这样作。
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];
}
复制代码
在这种状况下,你须要在 for 循环的范围内发送 allocedNumber 释放消息以平衡 alloc。因为数组在 addObject: 添加时 retained 了数字,所以在数组中它不会被释放。
要理解这一点,请将本身置于实现集合类的人的位置。你但愿确保没有给予你照顾的对象从你的下方消失,所以你在传入时向他们发送 retain 消息。若是它们被删除,你必须发送 release 消息以保持平衡,在你本身的 dealloc 方法中,应该向任何剩余的对象发送一条 release 消息。
全部权策略经过引用计数实现 - 一般在 retain 方法以后称为“retain count”。每一个对象都有一个 retain count。
建立对象时,其 retain count 为1。
向对象发送 retain 消息时,其 retain count 将增长1。
向对象发送 release 消息时,其 retain count 减1。
当你向对象发送 autorelease 消息时,其 retain count 在当前自动释放池的末尾递减1。
若是对象的 retain count 减小到零,则将其销毁。
重要提示:应该没有理由明确询问对象的 retain count 是什么。结果一般会产生误导,由于你可能不知道哪些框架对象 retained 了你感兴趣的对象。在调试内存管理问题时,你应该只关心确保代码遵照全部权规则。
自动释放池块提供了一种机制,你能够放弃对象的全部权,但避免当即释放它(例如从方法返回对象时)。一般,你不须要建立本身的自动释放池块,但在某些状况下,你必须或者这样作是有益的。
使用 @autoreleasepool 标记 autorelease pool block,如如下示例所示:
@autoreleasepool {
// Code that creates autoreleased objects.
}
复制代码
在 autorelease pool block 的末尾,在块中接收到 autorelease 消息的对象被发送 release 消息 - 对象在每次在块内发送 autorelease 消息时接收 release 消息。
与任何其余代码块同样,autorelease pool blocks 能够嵌套:
@autoreleasepool {
// . . .
@autoreleasepool {
// . . .
}
. . .
}
复制代码
(你一般不会彻底按照上面的方式看到代码;一般,一个源文件中的 autorelease pool block 中的代码将调用另外一个源文件中的包含在 autorelease pool block 中的代码。)对于给定的 autorelease 消息,相应的 release 消息在 autorelease pool block 的末尾向发送过 autorelease 消息的对象发送。
Cocoa 老是但愿代码在 autorelease pool block 中执行,不然自动释放的对象不会被释放而应用程序会泄漏内存。(若是你在 autorelease pool block 以外发送 autorelease 消息,Cocoa 会记录一个合适的错误消息。)AppKit 和 UIKit 框架处理 autorelease pool block 中的每一个事件循环迭代(例如鼠标按下事件或敲击)。所以,你一般没必要本身建立 autorelease pool block,甚至没必要查看用于建立池的代码。可是,有三种状况可能会使用你本身的自动释放池块:
若是你正在编写不基于 UI 框架的程序,例如命令行工具。
若是编写一个建立许多临时对象的循环。
你能够在循环内使用 autorelease pool block 在下一次迭代以前处理这些对象。在循环中使用 autorelease pool block 有助于减小应用程序的最大内存占用量。
若是你产生一个辅助线程。
一旦线程开始执行,你必须建立本身的 autorelease pool block; 不然,你的应用程序将泄漏对象。
许多程序建立自动释放的临时对象。这些对象会添加到程序的内存占用空间,直到块结束。在许多状况下,容许临时对象累积直到当前事件循环迭代结束时不会致使过多的开销; 可是,在某些状况下,你可能会建立大量临时对象,这些对象会大大增长内存占用,而且你但愿更快地处置。在后面这些状况下,你能够建立本身的 autorelease pool block。在块结束时,临时对象被释放,这一般致使它们的释放,从而减小程序的内存占用。
如下示例显示了如何在 for 循环中使用 local autorelease pool block。
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
复制代码
for 循环一次处理一个文件。在 autorelease pool block 内发送 autorelease 消息的任何对象(例如 fileContents)在块结束时释放。
在 autorelease pool block 以后,你应该将块中自动释放的任何对象视为“已处置”。不要向该对象发送消息或将其返回给你的方法的调用者。若是必须使用 autorelease pool block 以外的临时对象,则能够经过向块内的对象发送保留消息,而后在块以后将其自动释放发送,如此示例所示:
– (id)findMatchingObject:(id)anObject {
id match;
while (match == nil) {
@autoreleasepool {
/* Do a search that creates a lot of temporary objects. */
match = [self expensiveSearchForObject:anObject];
if (match != nil) {
[match retain]; /* Keep match around. */
}
}
}
return [match autorelease]; /* Let match go and return it. */
}
复制代码
发送 retain 以在自 autorelease pool block 中匹配并在 autorelease pool block 延长匹配的生命周期后向其发送自动释放,并容许它在循环外接收消息并返回到 findMatchingObject: 的调用者。
Cocoa 应用程序中的每一个线程都维护本身的 autorelease pool blocks 栈。若是你正在编写仅基于 Foundation 的程序或者分离线程,则须要建立本身的 autorelease pool block。
若是你的应用程序或线程长寿而且可能生成大量自动释放的对象,则应使用 autorelease pool blocks(如主线程上的 AppKit 和 UIKit); 不然,自动释放的对象会累积,而且你的内存占用会增长。若是你的分离线程没有进行 Cocoa 调用,则不须要使用 autorelease pool block。