iOS 内存管理编程指南

概念

Application Memory Management(应用程序内存管理) 是在程序运行时分配内存、使用内存并在完成后释放内存的过程。编写良好的程序将使用尽可能少的内存。在 Objective-C 中,Memory Management也可以看作是将有限内存资源的所有权分配给许多数据和代码的一种方式。

尽管通常在单个对象级别上考虑 Memory Management,但实际上你的目标是管理 object graphs(对象图)。你要确保内存中没有超出实际需要的对象。

Objective-C 提供了两种App Memory Management的方法:

1、在本指南中描述的方法,称为 manual retain-release(手动保留释放) 或 MRR,可以通过跟踪自己拥有的对象来显式管理内存。这是使用 Foundation 中的 NSObject类 与 runtime environment 一起提供的一个称为 reference counting(引用计数) 的模型来实现的。

2、在 Automatic Reference Counting(ARC) 中,系统使用与 MRR 相同的 reference counting system,但是它会在编译时为你插入适当的内存管理方法调用。有关 ARC 的更多信息,请参阅 Transitioning to ARC Release Notes

良好做法可防止与内存相关的问题。错误的 memory management 会导致的问题主要有两种:

  • 释放或覆盖仍在使用的数据

这会导致内存损坏,通常会导致你的 App 崩溃,或者更糟的是,损坏的用户数据。

  • 不释放不再使用的数据会导致 memory leaks(内存泄漏)

memory leak是指分配的内存不会被释放,即使它不再被使用。泄漏会导致你的 App 使用越来越多的内存,进而可能导致系统性能下降或应用程序被终止。

但是,从 reference counting 的角度考虑 memory management 通常会适得其反,因为你倾向于根据实现细节而不是实际目标来考虑 memory management。相反,你应该从对象所有权和 object graphs 的角度考虑 memory management

Cocoa 使用一个直截了当的 naming convention(命名约定) 来指示你何时拥有一个方法返回的对象。请参阅 Memory Management Policy

Memory Management Policy

reference-counted environment(引用计数环境) 中用于 memory management 的基本模型 由 NSObject protocol 中定义的方法 和 标准方法 naming convention 的组合 提供。NSObject类还定义了一个 dealloc 方法,该方法在对象被释放后自动调用。

Basic Memory Management Rules

memory management model 基于对象所有权。任何对象都可以具有一个或多个所有者。只要一个对象至少具有一个所有者,它就会继续存在。如果一个对象没有所有者,则 runtime system 会自动销毁它。为了确保你清楚何时拥有对象,何时不拥有对象,Cocoa 设置了以下策略:

  • 你拥有自己创建的任何对象。

使用名称以 allocnewcopymutableCopy 开头的方法(例如allocnewObjectmutableCopy)创建对象。

  • 你可以使用 retain 来获取对象的所有权

接收到的对象通常保证在接收到它的方法内保持有效,并且该方法还可以安全地将该对象返回给其调用者。在以下两种情况下你使用 retain:(1)在访问器方法或 init 方法的实现中,将要存储的对象的所有权作为属性值;(2)防止对象由于其他操作的副作用而失效(如 Avoid Causing Deallocation of Objects You’re Using))。

  • 当你不再需要它时,必须放弃对你拥有的对象的所有权

可以通过发送 release 消息或 autorelease 消息来放弃对象的所有权。因此,在Cocoa术语中,放弃对象的所有权通常称为 “releasing” an object

  • 你不得放弃 你不拥有的对象 的所有权

这只是先前明确规定的政策规则的推论。

如下是对象内存释放的一些例子:

发送 release

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

Person 对象是使用 alloc 方法创建的,因此当不再需要它时,将向它发送 release 消息。person’s name不是用任何拥有的方法来检索,因此不会发送 release 消息。但是请注意,该示例使用 release 不是 autorelease

使用 autorelease 发送延迟 release

当需要发送延迟的release消息时(通常是从方法返回对象时),可以使用 autorelease

如下示例,你拥有 alloc 返回的字符串。为了遵守 memory management rules,你必须放弃对字符串的所有权,然后再丢失对该字符串的引用。但是,如果使用 release,则字符串将在返回之前被释放(该方法将返回无效的对象)。使用 autorelease,表示你要放弃所有权,但是允许方法的调用者在释放之前使用返回的字符串。

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

你也可以像这样实现fullName方法:

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

遵循基本规则,你不拥有 stringWithFormat: 返回的字符串,因此可以安全地从该方法返回该字符串。

相比之下,以下实现是错误的:

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

根据 naming convention(命名约定),没有任何东西表示 fullName 方法的调用者拥有返回的字符串。因此,调用者没有理由释放返回的字符串,因此它将被泄漏。

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

Cocoa 中的某些方法指定对象是通过引用返回的(即,它们采用 ClassName **id * 类型的参数)。一种常见的模式是使用 NSError 对象,该对象包含发生错误时的相关信息,如initWithContentsOfURL:options:error: (NSData) 和 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 术语中,它是“freed”“deallocated.”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

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

注意:
切勿直接调用另一个对象的 dealloc 方法。
必须在dealloc 方法实现结束时调用父类的实现。
你不应该将系统资源的管理与对象生存期联系起来;Don’t Use dealloc to Manage Scarce Resources
App 终止时,可能不会向对象发送 dealloc 消息。由于进程的内存在退出时会自动清除,因此简单地允许操作系统清理资源比调用所有 memory management 方法更有效。

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

Core Foundation 对象也有类似的 memory management rules(请参阅 Memory Management Programming Guide for Core Foundation)。但是,CocoaCore Foundationnaming conventions(命名约定) 不同。特别是,Core FoundationCreate Rule 不适用于返回 Objective-C 对象的方法。例如,在以下代码片段中,你不负责放弃myInstance的所有权:

1
MyClass *myInstance = [MyClass createInstance];

实用的内存管理

尽管 Memory Management Policy(内存管理策略) 中描述的基本概念很简单,但是你可以采取一些实际步骤,使管理内存更容易,并帮助确保你 App 保持可靠和健壮,同时最大程度地减少其资源需求。

使用 Accessor Methods 使内存管理更轻松

如果你定义类的属性是对象,则必须确保在使用该对象时,不会释放任何设置为该值的对象。因此,在设置对象时必须声明该对象的所有权。你还必须确保随后放弃对当前持有 Value 的所有权。

有时它可能看起来很乏味或繁琐,但是如果你始终使用accessor methods(访问器方法),则出现内存管理问题的可能性会大大降低。如果在整个代码中对实例变量使用retainrelease,则你几乎肯定在做错误的事情。

如下是你考虑一个 Counter 对象,你要设置其计数。

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

property(属性) 声明了两个 accessor methods。通常,你应该要求编译器合成这些方法;然而,看看如何实现它们是很有启发性的。

“get” accessor 中,你只需返回合成的实例变量,因此不需要 retainrelease

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

“set” method 中,如果其他所有人都遵循相同的规则,则必须假定可以随时处置新计数,因此必须通过发送 retain 消息来获取对象的所有权,以确保不会被处置。你还必须在此处通过发送 release 消息来放弃对旧计数对象的所有权。(在 Objective-C 中允许将消息发送到 nil,因此,如果尚未设置_count,则实现仍将起作用。)如果两个对象是同一对象,则必须在 [newCount retain] 之后发送该消息,否则您将无法发送该消息。 希望在不经意间导致它被释放。

1
2
3
4
5
6
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// 进行新工作
_count = newCount;
}

使用 Accessor Methods 设置属性值

假设你要实现一种重置 counter 的方法。有两种选择。第一个实现使用 alloc 创建 NSNumber 实例,因此你可以使用 release 来平衡它。

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

第二种方法使用一个 convenience constructor(便利构造函数) 来创建一个新的 NSNumber 对象。因此,不需要 retainrelease 消息

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

以上两种方法都是用了 accessor method

对于简单的情况,下面的方法几乎可以肯定是正确的,但是尽量避免使用 accessor methods 有诱惑力,但这几乎肯定会在某个阶段导致错误(例如,当您忘记 retainrelease 时,或者如果实例变量的内存管理语义发生更改时)。

还要注意,如果使用 key-value observing,则如下以这种方式更改变量不符合KVO。

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

不要在 Initializer 和 dealloc 方法中使用 Accessor Methods

你不应该使用 accessor methods 来设置实例变量的唯一地方是在 initializerdealloc 方法中。要用表示 0 的 number对象初始化 counter 对象,可以实现如下的 init 方法:

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

要允许使用非零计数初始化 counter,可以实现 initWithCount: 方法,如下所示:

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

由于 Counter 类有一个对象实例变量,因此你还必须实现 dealloc 方法。它应通过向其发送 release 消息来放弃任何实例变量的所有权,并最终应调用 super的实现:

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

使用弱引用以避免 Retain Cycles

Retain 对象会创建对该对象的 strong reference。在所有 strong references 释放之前,都无法释放对象。因此,如果两个对象可能具有 cyclical references(循环引用),则可能会产生一个称为 retain cycle 的问题,也就是说,它们彼此之间有着 strong reference。(直接或通过一系列其他对象,每个对象都强烈引用下一个,导致第一个回到第一个)

如下是 cyclical references 的图解:

解决 retain cycles 问题的方法是使用 weak references(弱引用)。weak reference 是一种非所有权关系,其中源对象不 retain(保留) 其具有引用的对象。

但是,为了保持 object graph 的完整性,在某个地方必须有 strong references(如果只有 weak references,则 pagesparagraphs 可能没有任何所有者,因此将被释放)。 因此,Cocoa建立了一个约定,即 父对象 应保持对其 子代 的 strong references,而 子对象 应具有对其 父代 的weak references

Cocoaweak references 的示例包括但不限于 table data sourcesoutline view itemsnotification observers以及 miscellaneous targetsdelegates

你需要谨慎地将消息发送到仅持有 weak reference 的对象。如果在对象被释放后向其发送消息,则应用程序将 crash(崩溃)。你必须对对象何时有效 具有明确定义的条件。

在大多数情况下,weak-referenced object知道另一个对象对它的弱引用,就像 circular references 一样,并负责在它释放时通知另一个对象。例如,向notification center注册对象时,notification center将存储对该对象的 weak reference,并在发布相应通知时向其发送消息。当对象被释放后,你需要在notification center注销它,以防止notification center向不再存在的对象发送任何消息。同样,当一个 delegate object 被释放时,你需要通过向另一个对象发送带有 nil参数的 setDelegate: 消息来删除 delegate link。这些消息通常从对象的 dealloc 方法发送。

避免释放正在使用的对象

Cocoa 的所有权策略规定,接收到的对象通常应在整个调用方法范围内保持有效。还应该可以从当前作用域返回接收到的对象,而不必担心它会被释放。对你的 App 来说,对象的 getter 方法返回缓存的实例变量或计算值并不重要。重要的是该对象在你需要的时间内保持有效。

该规则偶尔会有例外,主要属于以下两类之一。

1、从一个基本集合类中删除一个对象时。

1
2
3
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject 现在可能无效

从一个基本集合类中删除一个对象时,将向它发送一个release(而不是 autorelease)消息。如果该集合是被移除对象的唯一所有者,那么被移除对象(示例中的 heisenObject )将立即被释放。

2、当“父对象”被释放时

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

在某些情况下,你将从另一个对象中检索一个对象,然后直接或间接释放父对象。如果释放父级导致将其释放,并且父级是子项的唯一所有者,(假设在父级的 dealloc 方法中向其发送了 release 而不是 autorelease 消息。) 那么子项(示例中的 heisenObject)将同时释放。

为了防止出现这些情况,请在接收到 heisenObject 后将其保留,并在完成后将其释放。如下:

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

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

通常,你不应该在 dealloc 方法中管理 scarce resources(稀缺资源),例如 file descriptorsnetwork connections 以及 bufferscaches。特别是,你不应设计类,以便在你认为 dealloc 将被调用时调用它。由于 bugApp tear-down(APP崩溃),对 dealloc 的调用可能会延迟或避开。

相反,如果您有一个类,其实例管理 scarce resources,则应设计你的 App,以使你知道何时不再需要这些资源,然后可以告诉实例在此时clean up。通常情况下,你会随后释放该实例,然后dealloc就会随之而来,但如果它不释放,则不会遇到其他问题。

如果你试图在 dealloc 的基础上进行resource management,则可能会出现问题。例如:

1、object graph的顺序依赖关系被分解。

object graph tear-down mechanism(对象图的拆卸机制)本质上是无序的。虽然你可能通常期望并得到一个特定的顺序,但你正在引入脆弱性。例如,如果某个对象意外地autoreleased而不是released,则拆卸顺序可能会更改,这可能会导致意外结果。

2、不回收 scarce resources

Memory leaks(内存泄漏) 是应该修复的错误,但通常不会立即致命。但是,如果在你期望释放 scarce resources 时没有释放它们,则可能会遇到更严重的问题。例如,如果你的 App 用尽了 file descriptors,则用户可能无法保存数据。

3、在错误的线程上执行清除逻辑。

如果一个对象在一个意外的时间被自动释放,它将在它所在的线程的 autorelease pool block 上被释放。对于只能从一个线程访问的资源来说,这很容易是致命的。

集合拥有它们包含的对象

将对象添加到集合(例如数组,字典或集合)时,该集合将拥有该对象的所有权。当从集合中删除对象或释放集合本身时,该集合将放弃所有权。因此,例如,如果要创建数字数组,则可以执行以下任一操作:

如下例子中,你没有调用alloc,因此无需调用release。不需要 retain 新的数字(convenienceNumber),因为数组会这样做。

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

如下这种情况下,你确实需要在 for loop 作用域内向 allocedNumber 发送 release 消息以平衡alloc。由于数组在由 addObject: 添加时会 retained(保留) 该数字,因此当它在数组中时将不会被释放。

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

Ownership Policy 是使用 Retain Counts 实现的

ownership policy(所有权策略) 是通过 reference counting(引用计数)实现的,通常在 retain 方法之后称为 “retain count”(保留计数)。每个对象都有一个retain count

  • 创建对象时,其 retain count 为1。
  • 向对象发送 retain 消息时,其 retain count 将增加1。
  • 向对象发送 release 消息时,其 retain count 减少1。
  • 向对象发送 autorelease 消息时,其 retain count 在当前 autorelease pool block 的末尾减少1。
  • 如果对象的 retain count 减少到零,则会将其释放。

应该没有理由显式地询问对象其保留计数是多少 (请参阅 retainCount)。结果往往是误导性的,因为你可能不知道哪些 framework objects 保留了你感兴趣的对象。在调试 memory management 问题时,只应确保代码符合 ownership rules

Autorelease Pool Blocks

Autorelease pool blocks(自动释放池块) 提供了一种机制,你可以通过该机制放弃对象的所有权,但可以避免立即释放对象的可能性(例如,从方法返回对象时)。通常,你不需要创建自己的autorelease pool block,但是在某些情况下,你必须这样做或者这样做是有益的。

使用 @autoreleasepool 标记 autorelease pool block,如以下示例所示:

1
2
3
@autoreleasepool {
// 创建自动释放对象的代码.
}

autorelease pool block的末尾,在 block 中接收到 autorelease 消息的对象会被发送一条 release 消息——每次在 block 中接收到 autorelease 消息时,对象都会收到一条 release 消息。

像任何其他代码块一样,autorelease pool block可以嵌套:

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

(你通常不会看到与上述完全相同的代码;通常,一个源文件中autorelease pool block中的代码会调用另一个autorelease pool block中包含的另一个源文件中的代码。)对于给定的 autorelease 消息,相应的release消息 将在发送 autoreleaseautorelease pool block 的末尾发送。

Cocoa 始终希望代码在 autorelease pool block 中执行,否则自动释放的对象将不会被释放,应用程序会leaks memory。(如果你在 autorelease pool block 之外发送 autorelease 消息,则 Cocoa 会记录一条适当的错误消息。)AppKitUIKit 框架处理autorelease pool block中的每个event-loop迭代(例如,鼠标按下事件或轻击)。因此,你通常不必自己创建一个autorelease pool block,甚至不必查看用于创建一个autorelease pool block的代码。但是,在三种情况下,你可能会使用自己的autorelease pool block

  • 如果你正在编写一个不基于UI框架的程序,例如命令行工具。

  • 如果编写一个创建许多临时对象的循环。

你可以在循环内使用 autorelease pool block 在下一次迭代之前处理这些对象。在循环中使用 autorelease pool block 有助于减少应用程序的最大内存占用。

  • 如果生成 secondary thread(辅助线程)。

一旦线程开始执行,你必须创建自己的autorelease pool block;否则,您的应用程序将泄漏对象。

使用 Local Autorelease Pool Blocks 来减少峰值内存占用量

许多程序会创建自动释放的临时对象。这些对象添加到程序的内存占用,直到块结束。在许多情况下,允许临时对象累积到当前事件循环迭代结束不会导致过多的开销;但是,在某些情况下,你可能会创建大量临时对象,这些临时对象会大大增加内存占用,并且你希望更快地对其进行处理。在后一种情况下,你可以创建自己的autorelease pool block。在该块的末尾,将释放临时对象,这通常导致它们的deallocation,从而减少了程序的内存占用量。

以下示例显示了如何在for循环中使用local autorelease pool block

for循环一次处理一个文件。在 autorelease pool block 内发送 autorelease 消息的任何对象(例如 fileContents )都在该块的末尾释放。

1
2
3
4
5
6
7
8
9
10
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之后,你应该将在该块内自动释放的所有对象都视为“disposed of.”。不要向该对象发送消息或将其返回给你的方法的调用者。如果必须在autorelease pool block之外使用临时对象,则可以通过向块内的对象发送retain消息,然后在该块之后将其自动释放,来执行此操作,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
– (id)findMatchingObject:(id)anObject {

id match;
while (match == nil) {
@autoreleasepool {

/* 进行搜索以创建许多临时对象. */
match = [self expensiveSearchForObject:anObject];

if (match != nil) {
[match retain]; /* Keep match around. */
}
}
}

return [match autorelease]; /* Let match go and return it. */
}

如上示例,在 autorelease pool block 内将 retain 发送到 match,并在 autorelease pool block 后向其发送 autorelease,可延长 match 的生命周期,并允许其在循环外接收消息并返回给 findMatchingObject: 的调用程序。

Autorelease Pool Blocks 和 Threads

Cocoa 应用程序中的每个线程都维护自己的autorelease pool blocks栈(stack)。如果你正在编写 仅限于 Foundation 的程序 或 detach thread(分离线程),则需要创建自己的autorelease pool block

如果你的应用程序或线程是long-lived,并且有可能生成大量autoreleased objects,则应使用autorelease pool blocks(例如 AppKit 和 UIKit 在主线程上所做的);否则,autoreleased objects会堆积,并且你的内存占用量也会增加。如果detach thread不进行 Cocoa 调用,则无需使用autorelease pool block

如果使用 POSIX thread APIs 而不是 NSThread 创建 secondary threads(辅助线程),则除非 Cocoa 处于多线程模式,否则无法使用 Cocoa。只有在分离其第一个 NSThread 对象之后,Cocoa 才进入多线程模式。要在 secondary POSIX threads 上使用 Cocoa,你的应用程序必须首先分离至少一个 NSThread 对象,该对象可以立即退出。您可以使用 NSThread 类方法isMultiThreaded 测试 Cocoa 是否处于多线程模式。

学习博客

About Memory Management

Rimson * Objective-C 内存管理简介

Bytedance * RickeyBoy | iOS Memory 内存详解 (长文)

一瓜技术 | iOS Memory 内存详解

清华同方 * 奉孝 | iOS 内存管理机制

即刻技术团队 | iOS 内存管理研究

文章作者: Czm
文章链接: http://yoursite.com/2020/05/25/iOS-%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E7%BC%96%E7%A8%8B%E6%8C%87%E5%8D%97/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm