第31条:在 dealloc 方法中只释放引用并解除监听

  本条要点:(作者总结)

  •  在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或 NSNOtificationCenter 等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定: 用完资源后必须调用 close 方法。
  • 执行异步任务的方法不应该在 dealloc 里调用; 只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。

  对象在经历其生命期后,最终会为系统所回收,这时就要执行 dealloc 方法了。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为 0 的时候。然而具体何时执行,则无法保证。也可以理解成: 我们能够通过人工观察保留操作与释放操作的位置。来预估此方法何时即将执行。但实际上,程序库会以开发者察觉不到的方式操作对象,从而使回收对象的真正时机和预期的不同。你决不应该自己调用 dealloc 方法,运行期系统会在适当的时候调用它。而且,一旦调用过 dealloc 之后,对象就不再有效了,后续方法调用均是无效的。

  那么,应该在 dealloc 方法中做什么呢?主要就是释放对象所拥有的引用,也就是把所有 Objective-C 对象都释放掉,ARC 会通过自动生成的 .cxx_destruct 方法,在 dealloc 中为你自动添加这些释放代码。对象所拥有的其他非 Objective-C 对象也要释放。比如 CoreFoundation 对象就必须手工释放,因为它们是由纯C 的API 所生成的。

  在 dealloc 方法中,通常还要做一件事,那就是把原来配置过低观测行为(observation behavior)都清理掉。如果用 NSNotificationCenter 给此对象订阅(register)过某种通知,那么一般应该在这里注销(unregister),这样的话,通知系统就不再把通知发给回收后的对象了。若是还向其发送通知,则必然会令应用程序崩溃。

  dealloc 方法可以这样写:

1 - (void)dealloc {
2 
3   CFRelease(coreFoundationObject);
4 
5   [[NSNotificationCenter defaultCenter] removeObserver:self];
6 
7 }

  请注意,如果手动管理引用计数而不使用 ARC  的话,那么最后还需调用 “[super dealloc]”。ARC 会自动执行此操作,这再次表明其比手动管理更简单、更安全。若选择手动管理,则还要将当前对象所拥有的全部 Objective-C 对象逐个释放。

  虽说应该于 dealloc 中释放引用,但是开销较大或系统内部稀缺的资源则不在此列。像是文件描述符(file descriptor)、套接字(socket)、大块内存等,都属于这种资源。不能指望 dealloc 方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。在这种情况下,如果非要等到系统调用 dealloc 方法时才释放,那么保留这些稀缺资源的时间就有些过长了,这么做不合适。通常的做法是,实现另外一个方法,当应用程序用完资源对象后,就调用此方法。这样一来,资源对象的生命周期就变得更为明确了。

  此方说,如果某对象管理着连接服务器所用的套接字,那么也许就需要这种 “清理方法”(clean method)。此对象可能要通过套接字连接到数据库。对于对象所属的类,其接口可以这样写:

1 #import <Foundation/Foundation.h>
2 
3 @interface EOCServerConnection : NSObject
4 
5 - (void)open:(NSString *)address;
6 
7 - (void)close;
8 
9 @end

  该类与开发者之间的约定是: 想打开连接,就调用 “open:” 连接使用完毕,就调用 close 方法。“关闭”操作必须在系统把连接对象回收之前调用,否则就是编程错误(programmer error),这与通过 “保留” 及 “释放” 操作来平衡引用计数是类似的。

  在清理方法而非 dealloc 方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的 dealloc 都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到 dealloc 消息。由于应用程序终止之后,其占用的资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不调用 dealloc 方法是为了优化程序效率。而这也说明系统未必会在每个对象上调用其 dealloc 方法。在 Mac OS X 及iOS 应用程序所对应的 application delegate 中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的 “清理方法”。

  在 Mac OS X 系统里,应用程序终止时会调用 NSApplicationDelegate 之中的下述方法:

1 - (void)applicationWillTerminate:(NSNotification *)notification;

  而在 iOS 系统里,应用程序终止时则会调用 UIApplicationDelegate 之中的下述方法:

1 - (void)applicationWillTerminate:(UIApplication *)application;

  如果对象管理着某些资源,那么在 dealloc 中也要调用 “清理方法”,以防止开发者忘了清理这些资源。忘记清理资源的情况经常会发生,所以最好能输出一行消息,提示程序员代码里含有编程错误。在系统回收对象之前,必须调用 close 以释放其资源,否则 close 方法就失去了意义了,因此,没有适时调用 close 方法就是编程错误,我们应该在 dealloc 中补上这次调用,以防泄漏内存。下面举例说明 close 与 dealloc 方法如何来写:

1   - (void)close {
2 
3   /*clean up resources*/
4 
5    _closed = YES;
6 
7   }
 1 - (void)dealloc {
 2 
 3   if (!_closed) {
 4 
 5   NSLog(@"ERROR: close was not called before dealloc!");
 6 
 7   [self close];
 8 
 9   }
10 
11 }

  有时可能不想只输出一条错误消息,而是要抛出异常来表明不调用 close 方法是严重的编程错误。

  编写 dealloc 方法时还需要注意,不要在里面随便调用其他方法。刚才那段范例代码中,dealloc 方法确实调用了另外一个方法,不过那是为了侦测编程错误而破例。无论在这里调用什么方法都不太应该,因为对象此时 “已近尾声”(in a winding-down state)。如果在这里所调用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。这会导致很多问题,且经常使应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉该任务已完成,而此时如果对象已摧毁,那么回调操作就回出错。

  请再注意一个问题:调用dealloc 方法的那个线程会执行“最终的释放操作” (final release),令对象的保留计数降为 0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc 里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于 “正在回收的状态”(deallocating state),为了指明此状况,运行期系统已经改动了对象内部的数据结构。

  在 dealloc 里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并与其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于 “键值观测” (Key-Value Observation, KVO) 机制的监控之下,该属性的观察者(observer) 可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。

END

posted @ 2017-08-10 00:20  鳄鱼不怕牙医不怕  阅读(415)  评论(0编辑  收藏  举报