Cocoa基本原理指南 Cocoa对象 生命周期
Cocoa基本原理指南 Cocoa对象 生命周期是本文要介绍的内容, Cocoa对象的生命周期(至少是潜在地)跨越不同的阶段。它需要被创建、初始化、和使用(就是其它对象向它发送消息),它可能被保持、拷贝、或者归档,并最终被释放和销毁。下面的讨论将给出一个典型对象的生命周期框图,但仍然不涉及太多的细节。
让我们从最后开始,即从清理对象的方式开始讨论。和其它编程语言不同,Objective-C没有自动释放不再使用的对象的“垃圾收集”设施。垃圾收集开销大而且不灵活,取而代之的是,Cocoa和Objective-C选择一种主动的、策略驱动的例程来保持对象,并在不再需要的时候进行清理。
这种例程和策略建立在引用计数的基础上。每个Cocoa对象都带有一个整数,用于指示对其持久性感兴趣的其它对象(甚至是例程代码的现场)的数目。这个整数被称为对象的保持数(“保持” 是为了避免和“引用”重复)。当您通过alloc或者allocWithZone:类方法创建对象的时候,Cocoa做了一些非常重要的工作:
它将对象的isa指针—NSObject类中唯一的公共实例变量—指向对象的类,因此将对象集成到类层次的运行时视图中(进一步信息请参见"对象的创建"部分)。
它将对象的保持数—一个隐藏的实例变量,所有对象都有—设置为1(这里假定对象的创建者对其持久性感兴趣)。
为对象分配内存之后,您通常需要将其实例变量设置为合理的初始值,以便进行初始化(NSObject声明了init方法作为这个目的的原型)。这样对象就可以使用了,您可以向它发送消息,将它传递给其它对象,等等。
请注意:由于除了显式分配的对象之外,初始化方法也可以返回一个对象,因此习惯上将alloc消息嵌套在init消息(或其它初始化方法)中—举例来说:
id anObj = [[MyClass alloc] init];
当您释放一个对象—也就是向对象发送一个release消息时—NSObject会减少它的保持数。如果保持数从1下降到0,对象就会被解除分配。对象的解除分配有两个步骤:首先是对象的dealloc方法被调用,以释放实例变量和动态分配的内存;然后是操作系统将对象的本身销毁,并回收对象占用的内存。
重要提示:您永远不应该直接调用对象的dealloc方法。
如果您不希望对象很快消失,该怎么办呢?如果您在创建对象之后向它发送一个retain消息,对象的保持数就会增加到2。这样,就需要两个release消息才能导致对象的释放。图2-3描述了这种极为简单的场景。
对象的生命周期—简化视图
当然,在这个场景下,对象的创建者不需要保持对象,它已经拥有对象了。但是,如果这个创建者要将对象传递给消息中的另一个对象,则情况就不一样了。在Objective-C程序中,人们假定从其它对象接收到的对象在其被得到的作用域内总是正当的。负责接收的对象可以向被接收的对象发送消息,而且可以将它传递给其它对象。这个假设要求对象的发送者“行为规矩”,不要在客户对象仍然拥有被发送对象的引用时将它过早释放。
如果客户对象在程序的作用域之外希望保持接收到的对象,则可以保持该对象—也就是向它发送一个retain消息。保持一个对象会增加该对象的保持数,从而表示希望拥有该对象。客户对象有责任在一段时间后释放该对象。如果对象的创建者将该对象释放,但同时有一个客户对象已经保持了该对象,则该对象会一直持续到客户对象将它释放为止。图2-4说明了这个序列:
保持接收到的对象
您可以不保持对象,而是通过发送copy或copyWithZone:消息来对其进行拷贝(很多子类—如果不是大多数的话—都封装了某种数据采纳方法,或遵循这种协议)。拷贝一个对象不仅仅是对其进行复制,而且几乎总是将它的保持数设置为1(请参见图2-5)。根据对象的本质和可能的用法,拷贝可以是浅拷贝,也可以是深拷贝。深拷贝将对象复制为被拷贝对象的一个实例变量,而浅拷贝只是复制那些实例对象的引用。
在用法方面,copy和retain的区别在于前者要求成为对象新的、唯一的拥有者;新的拥有者可以修改拷贝后的对象,而不考虑其原始对象。一般地说,您需要对值对象(即对某些简单的值进行封装的对象)进行拷贝,而不是保持。特别是当对象是可变的时候,比如一个NSMutableString对象。对于不可变对象,copy和retain可能是等价的,其实现方法也是类似的。
拷贝接收到的对象
您可能已经注意到,用这种策略管理对象的生命周期有一个潜在的问题,就是创建一个对象并将它传递给另一个对象的对象本身并不总是知道什么时候可以安全地释放对象。在调用堆栈中可能有多个该对象的引用,某些引用可能来自创建者不知道的对象。如果创建者对象释放了其所创建的对象,而其它对象向这个已经被销毁的对象发送消息,程序就会崩溃。为了解决这个问题,Cocoa引入了一种延迟对象释放的机制,称为自动释放(autoreleasing)机制。
自动释放机制通过自动释放池(由NSAutoreleasePool类定义)来实现。自动释放池是位于显式定义的作用域内的一个对象集合,该作用域被标志为最后释放。自动释放池可以嵌套。当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放(参见图2-6)。如果您正在开发应用程序,可能不需要建立一个自动释放池,Application Kit会自动建立一个自动释放池,其作用域为为应用程序的事件周期。
自动释放池
到目前为止,对象生命周期的讨论主要关注整个周期中的对象管理机制。但是,指导如何使用这些机制的是对象的所有权策略。这个策略可以概括如下:
如果您通过分配和初始化(比如[[MyClass alloc] init])的方式来创建对象,您就拥有这个对象,需要负责该对象的释放。这个规则在使用NSObject的便利方法new时也同样适用。
如果您拷贝一个对象,您也拥有拷贝得到的对象,需要负责该对象的释放。
如果您保持一个对象,您就部分拥有这个对象,需要在不再使用时释放该对象。
反过来,
如果您从其它对象那里接收到一个对象,则您不拥有该对象,也不应该释放它(这个规则有少数的例外,在参考文档中有显式的说明)。
和其它规则一样,这些策略也有一些例外和经常出错的地方:
如果您通过类工厂方法来创建对象(比如NSMutableArray arrayWithCapacity:方法),则可以假定您接收到的对象已经自动被放到自动释放池了。您不应该自行将它释放,如果您需要保持该对象,则应该保持(retain)它。
为了避免循环引用,子对象不能保持它的父对象(父对象是该子对象的创建者,或者将该子对象作为实例变量持有的对象)。
请注意:在上面的原则中提到的“释放”是指向对象发送一个release或autorelease消息。
如果您没有遵循这个所有权的策略,则可能导致您的Cocoa程序出现两种不好的结果:由于没有释放自己创建、拷贝、或保持的对象,您的程序会发生内存泄露;或者,由于向已经解除分配的对象发送消息,您的程序发生崩溃。而且还会有进一步的问题:调试这些问题可能相当费时间。
对象的生命周期中可能发生的另一个基本事件是归档。归档是将组成一个面向对象程序中的相关对象形成的网状结构—对象图—转化为一种可持久的形式(通常是一个文件),该形式可以保存对象图中对象的标识和彼此之间的关系。在解档时,可以通过这个档案重新构造出程序的对象图。为了参与归档(和解档),对象必须支持通过NSCoder类定义的方法对实例变量进行编码(和解码)。为了这个目的,NSObject采纳了NSCoding协议。有关对象归档的更多信息,请参见"对象的归档"部分。