MENU

Block 形式的通知中心观察者是否需要手动注销

melvin-tan-692281-unsplash.jpg

转载自个人在 SwiftGG 翻译的文章)

简单回答:需要 (在 iOS 11.2 上验证过)

几周之前,我在 twitter 上提出了一个问题

在 iOS 11 中是否还需要手动移除 block 形式的通知观察者?苹果在开发文档中有着自相矛盾的描述。addObserver(forName:object:queue:using:) 中说需要,而 removeObserver(_:) 中表明 iOS 9 之后都不在需要。

虽然我没有统计准确的数字,但是大致看来持不同意见的人差不多五五开。

所以下面我们就来具体测试验证一下。

问题

首先,该 block 形式的 API 是 NotificationCenter. addObserver (forName: object: queue: using:) 。通过该 API 我们在通知中心注册了一个函数用于处理对应的通知,并且得到一个表示观察者的返回值。

class MyObserver {
    var observation: Any? = nil

    init() {
        observation = NotificationCenter.default.addObserver(
            forName: myNotification, object: nil, queue: nil) { notification in
                print("Received \(notification.name.rawValue)")
            }
    }
}

问题是:当代码中的返回值 observation 销毁时(例如,MyObserver 实例对象析构了),通知中心会不会自动忽略并停止调用处理函数?毕竟[新版的 KVO 接口]中当观察者销毁后,响应处理是不会被调用。

或者,我们依旧需要手动调用 NotificationCenter. removeObserver(_:)(例如,在 MyObserver 的析构函数 deinit 手动注销)。

文档中的说明

基于 selector 形式的观察 API addObserver(_:​selector:​name:​object:) 的注销操作在 iOS 9 和 OSX 10.11 之后已经变成可选了。然而在 Foundation 发布注意事项中明确表明 Block 形式的 API 依然需要进行手动注销操作。

通过 -\[NSNotificationCenter addObserver​ForName:​object:​queue:​usingBlock:\] 形式的观察者在无用时依然需要进行注销操作,否则系统会保留对该观察者的强引用。

该文档发布之后是否存在新变化呢?

addObserver(forName:​object:​queue:​using:) 文档中依然明确了注销操作的必要性:

所有通过 addObserver(forName:​object:​queue:​using:) 创建的观察者在析构时都需要调用 removeObserver(\_:)](https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver) 或者 [removeObserver(\_:​name:​object:) 进行注销操作。

然而 removeObserver(_:) 文档中则有着相反的描述:

如果你的 APP 运行在 iOS 9 或者 macOS 10.11 及最新的版本上的话则不需要调用该 API 进行注销操作。

该文档中并没有对 selector 或者 block 进行区分说明,也就是说该操作同时适用于两者。

进行测试验证

通过我写的测试应用,你可以得到验证上诉问题(通过 Xcode 的终端输出)。

下面是通过测试得到的一些结论:

  • block 形式得到的观察者依然需要进行手动注销操作(即使在 iOS 11.2 上),所以 removeObserver (_:) 文档存在明显的误导。

  • 如果没有进行注销操作的话,那么 block 就会被一直持有而且依然能够被相关通知触发执行。此时该行为对 APP 的潜在威胁取决于 block 内部持有的对象。

  • 即使你在 deinit 中调用了注销操作,你依旧需要注意 block 中不能捕获 self 引用,否则会造成循环引用此时 deinit 也永远不会得到执行。

自动注销

那么如何优雅的处理这个问题呢?我的建议是:对观察对象进行一次封装。该封装类型的指责就是保持观察者对象并且在析构函数中自动将其注销。

/// Wraps the observer token received from 
/// NotificationCenter.addObserver(forName:object:queue:using:)
/// and unregisters it in deinit.
final class NotificationToken: NSObject {
    let notificationCenter: NotificationCenter
    let token: Any

    init(notificationCenter: NotificationCenter = .default, token: Any) {
        self.notificationCenter = notificationCenter
        self.token = token
    }

    deinit {
        notificationCenter.removeObserver(token)
    }
}

通过封装处理,我们将观察者的生命周期和该类型实例进行了绑定。接下来我们只需要将该封装类型实例通过私有属性进行保存,那么其持有者就会 deinit 触发时销毁该封装实例紧接着销毁观察者实例对象。这样就不需要在代码中对其进行手动注销操作了。另外我们还可以将该实例声明为 Optional ,这样通过将其设置为 nil 也能进行手动注销操作。该模式被称为 资源获取即初始化 (RAII)。

最后通过对 NotificationCenter 进行拓展以实现该封装工作的自动化。

extension NotificationCenter {
    /// Convenience wrapper for addObserver(forName:object:queue:using:)
    /// that returns our custom NotificationToken.
    func observe(name: NSNotification.Name?, object obj: Any?, 
        queue: OperationQueue?, using block: @escaping (Notification) -> ())
        -> NotificationToken
    {
        let token = addObserver(forName: name, object: obj, queue: queue, using: block)
        return NotificationToken(notificationCenter: self, token: token)
    }
}

如果此时将原有的 addObserver(forName:​object:​queue:​using:) 替换为上诉新 API ,并将得到 NotificationToken 实例通过属性保存的话,你将不再需要手动注销操作了。

Chris 和 Florian 也在 Swift Talk episode 27: Typed Notifications 中提到过该技术,我强烈建议你去听一听。

标签: 无
添加新评论

已有 1 条评论
  1. 明天去看你 明天去看你

    老哥好久没更新啊,我来留言咯...