Search code examples
iosswiftnsnotificationcenternotificationcenteraddobserver

iOS NotificationCenter unexpected retained closure


In the documentation, it says:

The block is copied by the notification center and (the copy) held until the observer registration is removed.

And it provides a one-time observer example code like so:

let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
var token: NSObjectProtocol?
token = center.addObserverForName("OneTimeNotification", object: nil, queue: mainQueue) { (note) in
    print("Received the notification!")
    center.removeObserver(token!)
}

Now I expect the observer to be removed as removeObserver(_:) is called, so my code goes like this:

let nc = NotificationCenter.default
var successToken: NSObjectProtocol?
var failureToken: NSObjectProtocol?

successToken = nc.addObserver(
    forName: .ContentLoadSuccess,
    object: nil,
    queue: .main)
{ (_) in
    nc.removeObserver(successToken!)
    nc.removeObserver(failureToken!)

    self.onSuccess(self, .contentData)
}

failureToken = nc.addObserver(
    forName: .ContentLoadFailure,
    object: nil,
    queue: .main)
{ (_) in
    nc.removeObserver(successToken!)
    nc.removeObserver(failureToken!)

    guard case .failed(let error) = ContentRepository.state else {
        GeneralError.invalidState.record()
        return
    }

    self.onFailure(self, .contentData, error)
}

Surprisingly, the self is retained and not removed.

What is going on?


Solution

  • Confirmed some weird behavior going on.

    First, I put a breakpoint on the success observer closure, before observers are removed, and printed the memory address of tokens, and NotificationCenter.default. Printing NotificationCenter.default shows the registered observers.

    I won't post the log here since the list is very long. By the way, self was captured weakly in the closures.

    Printing description of successToken:
    ▿ Optional<NSObject>
      - some : <__NSObserver: 0x60000384e940>
    Printing description of failureToken:
    ▿ Optional<NSObject>
      - some : <__NSObserver: 0x60000384ea30>
    

    Also confirmed that observers were (supposedly) removed by printing NotificationCenter.default again after the removeObserver(_:)s were invoked.

    Next, I left the view controller and confirmed that the self in the quote code was deallocated.

    Finally, I turned on the debug memory graph and searched for the memory addresses and found this:

    enter image description here

    In the end, there was no retain cycle. It was just that the observers were not removed, and because the closures were alive, the captured self was alive beyond its life cycle.

    Please comment if you guys think this is a bug. According to the documentation on NotificationCenter, it most likely is...