内存管理工作原理
在内存管理的Objective-C代码里,一个Cocoa对象存在于一个生命周期,有明确的阶段。它被创建,初始化,并使用(也就是,其它对象发 送消息给它)。它还可能会被保留,拷贝,或压缩,并最终被释放和销毁。下面的讨论以图表形式对一个典型对象的生命周期进行了描述,这里还没有涉及更多的细 节。
让我们从后面开始,当垃圾收集被关掉时对象销毁的方式。在此背景下Cocoa和Objective-C 选择一个自动的,策略驱动的过程来保持对象的存在并在不再被需要的时候销毁它们。
这个过程和策略依赖于引用计数的概念。每个Cocoa对象携带一个整数用来指示对其存在感兴趣的其它对象的数目。这个整数被称为对象的保留数(retain count)(“retain”用来避免和术语“reference”重叠)。 当你创建一个对象时,或者通过一个类工厂方法或者使用alloc
或allocWithZone:
类方法, Cocoa 做了一些很重要的事情:
- 它设置对象的
isa
指针-NSObject
类的唯一公共成员变量-以指向这个对象的类,这样把这个对象集成到运行时视图类层次。(参见对象创建“Object Creation”获取更多信息) - 它设置对象的保留数(retain count)- 一种由运行时管理的隐藏的成员变量- 为1。(这里假设一个对象的创建者对其存在感兴趣)
在对象分配后,你一般会设置它的成员变量为一个合理的初始值。 (NSObject
声明init
方法作为这个目的的原形)。 这个对象现在已经可以使用了;你可以发送消息给它,把它传递给其他对象,等等。
注意: 因为一个初始化器可以返回一个不是显式声明的那个对象,惯例是嵌套alloc
消息表达式在init
消息里(或者其他初始化器)- 比如:
1 | <code>id anObj = [[MyClass alloc] init];</code> |
当你释放一个对象- 也就是,发送一个release
消息给它 – NSObject
减少其保留数。如果这个保留数从1变成0,这个对象会被释放。释放分成两个步骤。首先,对象的dealloc
方法被调用来释放成员变量并动态释放分配的内存。然后操作系统销毁对象自身并回收该对象曾经占用的内存。
重要: 你永远不该直接调用一个对象的dealloc
方法。
要是你不想一个对象马上消失?如果你在从别处接收到一个对象时给它发送了一个retain
消息,这个对象的保留数(retain count)被增加为2。现在在释放之前需要两个release
消息。图2-4 图示了这个相对简化的场景。
Figure 2-4 一个对象的生命周期- 简化视图
当然,在这个场景中,一个对象的创建者不需要保留这个对象。它早就拥有了这个对象。但是如果这个创建者在一个消息中传递这个对象给另外的对象,情况 就发生了变化。在一个Objective-C 程序中,一个接收一些其他对象的对象总是假设在其获得的范围内有效。这个接收对象可以发送消息给被接受的对象以及传递给其他对象。这个假设需要发送对象运 转并且不会过早的释放这个对象,当一个客户对象有一个指向它的引用时。
如果客户对象想在接收到的对象程序访问范围之外保留它,可以retain 它- 也就是,发送一个retain
消息给它。保留一个对象增加其保留计数,并由此表达该对象的一个所有权。这个客户对象假设稍后释放该对象的一个职责。如果一个对象的创建者释放它,但是一个客户对象保留了这个相同的对象,这个对象保持存在直到这个客户释放了它。图2-5 说明了这个顺序:
Figure 2-5 保留一个接收到的对象
和保留一个对象相反,你可以通过给它发送一个copy
或copyWithZone:
消 息来拷贝它。(很多子类,如果不是大多数,封装了一些采用或符合这个协议的数据)。拷贝一个对象不仅复制它而且常常总是重置它的保留计数为1(参见图 2-6)。拷贝可以是浅拷贝也可以是深拷贝,这依赖于这个对象的本质以及它的预期用途。一个深拷贝复制出一个可以承担成员变量相同作用的对象,而浅拷贝仅 仅增加这些成员变量的引用。
谈到使用,区别一个copy
和retain
的是前者声称这个对象的单独使用权;新的拥有者可以改变这个拷贝对象而无须关心它的原始对象。一般而言你拷贝一个对象而不是保留它,当它是一个数值对象- 也就是,一个对象封装了一些基本数据(如整数)。特别是这个对象本身是可变的,比如一个NSMutableString
,对于非可变对象,copy
和retain
可以等同并且也许可以用类似方法来实现。
Figure 2-6 拷贝一个接收到的对象
你 也许注意到了这个机制关于管理对象生命周期的一个潜在的问题。创建了一个对象并传递给另外的对象的这个创建者对象并不总是知道什么时候可以安全的释放掉这 个被创建出来的对象。有可能在堆栈中有这个对象的多个引用,有一些是创建者对象所不知道的。如果这个创建者对象释放掉这个被创建的对象然后其他对象给这个 已销毁对象发送消息的话,程序将崩溃。为了消除这个问题,Cocoa 引入了一个延迟释放的机制叫做autoreleasing。
Autoreleasing 使用自释放池(autorelease pools) (以NSAutoreleasePool
类定义)。一个自释放池是一个明确定义了范围的对象集合,这个范围标记着最终什么时候释放。自释放池可以被嵌套。当你发送一个 autorelease
消息, 一个该对象的引用被放进最近的自释放池中。它仍然是一个有效的对象,所以其他在自释放池定义范围内的对象可以给它发送消息。当程序执行到范围末尾时,这个 池被释放,而且,相应的,池中的所有对象也将被释放(参见图2-7)。如果你在开发一个应用程序你可能不需要建立一个自释放池,因为应用程序工具箱 (Application Kit)会自动建立一个范围为应用程序事件周期的自释放池.
Figure 2-7 一个自释放池
iPhone OS 提示: 因为在iPhone OS 中,应用程序在一个更加内存受限的环境中运行,所以不鼓励在应用程序创建很多对象的方法或代码段中使用自释放池(比如,循环)。相反,你应该在任何可能的时候显式的释放对象。
到目前为止关于对象生命周期的讨论集中在贯穿周期的对象管理机制上。但是一个对象拥有者策略指导如何使用这些机制。这个策略可以总结如下:
- 如果你通过分配并初始化来创建( create )一个对象(比如
[[MyClass alloc] init]
),你将拥有这个对象并负责释放它。这个规则同样适用于使用NSObject 简便方法(convenient method)new
。 - 如果你拷贝(copy)一个对象,你将拥有这个拷贝的对象并负责释放它。
- 如果你保留( retain )一个对象,你拥有该对象部分的所有权并且当你不需要的时候释放它。
相反的,
- 如果你从其他一些对象接收一个对象,你不拥有这个对象并且不应该释放它。(这个规则有一些少数的例外,已在参考文档中显式的标注)
和任何规则集一样,有一些例外和已知问题(“gotchas”):
- 如果你通过类工厂方法创建了一个对象(比如
NSMutableArray
arrayWithCapacity:
方法),假设你接收的这个对象是自动释放的。你不应该自己释放这个对象而且如果你想保持其存在的话应该retain它。 - 为了避免循环引用,一个子对象永远不该retain它的父对象。(一个父对象是这个子对象的创建者或者一个以成员变量包含该这个子对象的对象。)
注意: 上面指南中的“Release”意味着发送一个release
消息或者一个autorelease
消息给一个对象。
如果你不遵循这个所有权策略,在你的应用程序中很可能会发生两件糟糕的事情。因为你没有释放创建,拷贝,或者保留的对象,你的应用程序将存在内存泄漏。或者当你给一个已从其他地方释放的对象发送消息时导致你的程序崩溃。这里还有一个警告:调试这些问题费时费力。
一个另外的可能发生在一个对象生命周期里的基本事件是归档(archiving)。归档把组成一个面向对象的程序的互相关联的对象网络-对象图-转 换成一个持久格式(通常是一个文件),保存了标识和每个图中对象的关系。当程序被解归档时,它的对象图从归档中重新构建。为了参与归档(和解归档),一个 对象必须能够编码(和解码)。它的成员变量使用NSCoder 类方法。 NSObject 采用NSCoding 协议来完成这个目的。更多关于对象归档的内容,请参见对象归档(“Object Archives”)。
英文链接:http://developer.apple.com/iphone/library/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/