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

原文连接:swift.gg/2018/07/26/…
做者:Ole Begemann
译者:BigNerdCoding
校对:pmst
定稿:CMB
html

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

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

在 iOS 11 中是否还须要手动移除基于 block 形式的通知观察者?苹果开发文档中比较模糊。addObserver(forName:object:queue:using:) 中说须要,而 removeObserver(_:) 中又代表 iOS 9 以后都不在须要。github

虽然我没有统计准确的数字,可是大体看来持不一样意见的人差很少五五开。express

因此下面咱们就来具体测试验证一下。swift

问题

首先,我所说的基于 block 的接口声明是 NotificationCenter.addObserver(forName: object: queue: using:) 。使用该 API 咱们在通知中心注册了一个函数用于处理对应的通知,而且获得一个表示观察者的返回值。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 实例对象析构了),通知中心会不会自动忽略并中止调用处理函数呢?毕竟基于 KeyPathKVO 新接口当观察者销毁后,响应处理再也不被调用,因此通知可能也被理解成是这样进行的。app

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

文档中的说明

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

经过 -[NSNotificationCenter addObserverForName:object:queue:usingBlock:_] 形式添加的block类型观察者在无用时依然须要进行注销操做,由于系统会保留对该观察者的强引用。

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

addObserver(forName:object:queue:using:) 文档说明部分也明确指出了注销操做是必要的:

全部经过 addObserver(forName:object:queue:using:) 建立的观察者在析构以前都须要调用 removeObserver(_:) 或者 removeObserver(_:name:object:) 进行注销操做。

然而 removeObserver(_:) 文档说明处彷佛与之相反:

若是你的 APP 运行在 iOS 9 或者 macOS 10.11 及最新的版本上的话则不须要注销这个观察者在它的析构方法。

该文档中并无对 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 <Notification​Token> ,这样经过将其设置为 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 中提到过该技术,我向你强烈的推荐它。

相关文章
相关标签/搜索