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 中提到過該技術,我向你強烈的推薦它。

相關文章
相關標籤/搜索