概念
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
设置了以下策略:
- 你拥有自己创建的任何对象。
使用名称以 alloc
、new
、copy
或 mutableCopy
开头的方法(例如alloc
,newObject
或mutableCopy
)创建对象。
- 你可以使用
retain
来获取对象的所有权
接收到的对象通常保证在接收到它的方法内保持有效,并且该方法还可以安全地将该对象返回给其调用者。在以下两种情况下你使用 retain
:(1)在访问器方法或 init
方法的实现中,将要存储的对象的所有权作为属性值;(2)防止对象由于其他操作的副作用而失效(如 Avoid Causing Deallocation of Objects You’re Using))。
- 当你不再需要它时,必须放弃对你拥有的对象的所有权
可以通过发送 release
消息或 autorelease
消息来放弃对象的所有权。因此,在Cocoa
术语中,放弃对象的所有权通常称为 “releasing” an object
。
- 你不得放弃 你不拥有的对象 的所有权
这只是先前明确规定的政策规则的推论。
如下是对象内存释放的一些例子:
发送 release
1 | { |
Person
对象是使用 alloc
方法创建的,因此当不再需要它时,将向它发送 release
消息。person’s name
不是用任何拥有的方法来检索,因此不会发送 release
消息。但是请注意,该示例使用 release
不是 autorelease
。
使用 autorelease 发送延迟 release
当需要发送延迟的release
消息时(通常是从方法返回对象时),可以使用 autorelease
。
如下示例,你拥有 alloc
返回的字符串。为了遵守 memory management rules
,你必须放弃对字符串的所有权,然后再丢失对该字符串的引用。但是,如果使用 release
,则字符串将在返回之前被释放(该方法将返回无效的对象)。使用 autorelease
,表示你要放弃所有权,但是允许方法的调用者在释放之前使用返回的字符串。
1 | - (NSString *)fullName { |
你也可以像这样实现fullName方法:
1 | - (NSString *)fullName { |
遵循基本规则,你不拥有 stringWithFormat:
返回的字符串,因此可以安全地从该方法返回该字符串。
相比之下,以下实现是错误的:
1 | - (NSString *)fullName { |
根据 naming convention
(命名约定),没有任何东西表示 fullName
方法的调用者拥有返回的字符串。因此,调用者没有理由释放返回的字符串,因此它将被泄漏。
你不拥有通过引用返回的对象
Cocoa
中的某些方法指定对象是通过引用返回的(即,它们采用 ClassName **
或 id *
类型的参数)。一种常见的模式是使用 NSError
对象,该对象包含发生错误时的相关信息,如initWithContentsOfURL:options:error:
(NSData) 和 initWithContentsOfFile:encoding:error:
(NSString)所示。
在这些情况下,适用的规则与前面描述的相同。当你调用任何这些方法时,你不会创建 NSError
对象,因此你不拥有该对象。因此,无需释放它,如下示例所示:
1 | NSString *fileName = <#Get a file name#>; |
实现 dealloc 以放弃对象的所有权
NSObject
类定义了一个 dealloc
方法,当对象没有所有者且其内存被回收时,该方法会自动调用——在 Cocoa
术语中,它是“freed”
或 “deallocated.”
。dealloc
方法的作用是释放对象自己的内存,并处理它所拥有的任何资源,包括任何对象实例变量的所有权。
以下示例说明了如何为 Person
类实现 dealloc
方法:
1 | @interface Person : NSObject |
注意:
切勿直接调用另一个对象的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)。但是,Cocoa
和 Core Foundation
的 naming conventions
(命名约定) 不同。特别是,Core Foundation
的 Create Rule 不适用于返回 Objective-C
对象的方法。例如,在以下代码片段中,你不负责放弃myInstance的所有权:
1 | MyClass *myInstance = [MyClass createInstance]; |
实用的内存管理
尽管 Memory Management Policy
(内存管理策略) 中描述的基本概念很简单,但是你可以采取一些实际步骤,使管理内存更容易,并帮助确保你 App
保持可靠和健壮,同时最大程度地减少其资源需求。
使用 Accessor Methods 使内存管理更轻松
如果你定义类的属性是对象,则必须确保在使用该对象时,不会释放任何设置为该值的对象。因此,在设置对象时必须声明该对象的所有权。你还必须确保随后放弃对当前持有 Value
的所有权。
有时它可能看起来很乏味或繁琐,但是如果你始终使用accessor methods
(访问器方法),则出现内存管理问题的可能性会大大降低。如果在整个代码中对实例变量使用retain
和release
,则你几乎肯定在做错误的事情。
如下是你考虑一个 Counter
对象,你要设置其计数。
1 | @interface Counter : NSObject |
property
(属性) 声明了两个 accessor methods
。通常,你应该要求编译器合成这些方法;然而,看看如何实现它们是很有启发性的。
在 “get” accessor
中,你只需返回合成的实例变量,因此不需要 retain
或 release
:
1 | - (NSNumber *)count { |
在 “set” method
中,如果其他所有人都遵循相同的规则,则必须假定可以随时处置新计数,因此必须通过发送 retain
消息来获取对象的所有权,以确保不会被处置。你还必须在此处通过发送 release
消息来放弃对旧计数对象的所有权。(在 Objective-C
中允许将消息发送到 nil
,因此,如果尚未设置_count
,则实现仍将起作用。)如果两个对象是同一对象,则必须在 [newCount retain]
之后发送该消息,否则您将无法发送该消息。 希望在不经意间导致它被释放。
1 | - (void)setCount:(NSNumber *)newCount { |
使用 Accessor Methods 设置属性值
假设你要实现一种重置 counter
的方法。有两种选择。第一个实现使用 alloc
创建 NSNumber
实例,因此你可以使用 release
来平衡它。
1 | - (void)reset { |
第二种方法使用一个 convenience constructor
(便利构造函数) 来创建一个新的 NSNumber
对象。因此,不需要 retain
或 release
消息
1 | - (void)reset { |
以上两种方法都是用了 accessor method
。
对于简单的情况,下面的方法几乎可以肯定是正确的,但是尽量避免使用 accessor methods
有诱惑力,但这几乎肯定会在某个阶段导致错误(例如,当您忘记 retain
或 release
时,或者如果实例变量的内存管理语义发生更改时)。
还要注意,如果使用 key-value observing
,则如下以这种方式更改变量不符合KVO。
1 | - (void)reset { |
不要在 Initializer 和 dealloc 方法中使用 Accessor Methods
你不应该使用 accessor methods
来设置实例变量的唯一地方是在 initializer
和 dealloc
方法中。要用表示 0 的 number
对象初始化 counter
对象,可以实现如下的 init
方法:
1 | - init { |
要允许使用非零计数初始化 counter
,可以实现 initWithCount:
方法,如下所示:
1 | - initWithCount:(NSNumber *)startingCount { |
由于 Counter
类有一个对象实例变量,因此你还必须实现 dealloc
方法。它应通过向其发送 release
消息来放弃任何实例变量的所有权,并最终应调用 super
的实现:
1 | - (void)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
,则 pages
和 paragraphs
可能没有任何所有者,因此将被释放)。 因此,Cocoa
建立了一个约定,即 父对象 应保持对其 子代 的 strong references
,而 子对象 应具有对其 父代 的weak references
。
Cocoa
中 weak references
的示例包括但不限于 table data sources
、outline view items
、notification observers
以及 miscellaneous targets
和 delegates
。
你需要谨慎地将消息发送到仅持有 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 | heisenObject = [array objectAtIndex:n]; |
从一个基本集合类中删除一个对象时,将向它发送一个release
(而不是 autorelease
)消息。如果该集合是被移除对象的唯一所有者,那么被移除对象(示例中的 heisenObject
)将立即被释放。
2、当“父对象”被释放时
1 | id parent = < |
在某些情况下,你将从另一个对象中检索一个对象,然后直接或间接释放父对象。如果释放父级导致将其释放,并且父级是子项的唯一所有者,(假设在父级的 dealloc
方法中向其发送了 release
而不是 autorelease
消息。) 那么子项(示例中的 heisenObject
)将同时释放。
为了防止出现这些情况,请在接收到 heisenObject
后将其保留,并在完成后将其释放。如下:
1 | heisenObject = [[array objectAtIndex:n] retain]; |
不要使用 dealloc 来管理稀缺资源
通常,你不应该在 dealloc
方法中管理 scarce resources
(稀缺资源),例如 file descriptors
,network connections
以及 buffers
或 caches
。特别是,你不应设计类,以便在你认为 dealloc
将被调用时调用它。由于 bug
或 App 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 | NSMutableArray *array = <#Get a mutable array#>; |
如下这种情况下,你确实需要在 for loop
作用域内向 allocedNumber
发送 release
消息以平衡alloc
。由于数组在由 addObject:
添加时会 retained
(保留) 该数字,因此当它在数组中时将不会被释放。
1 | NSMutableArray *array = <#Get a mutable array#>; |
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 | @autoreleasepool { |
在autorelease pool block
的末尾,在 block
中接收到 autorelease
消息的对象会被发送一条 release
消息——每次在 block
中接收到 autorelease
消息时,对象都会收到一条 release
消息。
像任何其他代码块一样,autorelease pool block
可以嵌套:
1 | @autoreleasepool { |
(你通常不会看到与上述完全相同的代码;通常,一个源文件中autorelease pool block
中的代码会调用另一个autorelease pool block
中包含的另一个源文件中的代码。)对于给定的 autorelease
消息,相应的release
消息 将在发送 autorelease
的 autorelease pool block
的末尾发送。
Cocoa
始终希望代码在 autorelease pool block
中执行,否则自动释放的对象将不会被释放,应用程序会leaks memory
。(如果你在 autorelease pool block
之外发送 autorelease
消息,则 Cocoa
会记录一条适当的错误消息。)AppKit
和 UIKit
框架处理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 | NSArray *urls = <# An array of file URLs #>; |
在autorelease pool block
之后,你应该将在该块内自动释放的所有对象都视为“disposed of.”
。不要向该对象发送消息或将其返回给你的方法的调用者。如果必须在autorelease pool block
之外使用临时对象,则可以通过向块内的对象发送retain
消息,然后在该块之后将其自动释放,来执行此操作,如下例所示:
1 | – (id)findMatchingObject:(id)anObject { |
如上示例,在 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
是否处于多线程模式。