funct7
funct7

Reputation: 3601

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?

Upvotes: 1

Views: 1538

Answers (3)

deekay
deekay

Reputation: 929

Recently I've run into similar problem myself.

This does not seem a bug, but rather undocumented feature of the token which (as you've already noticed) is of __NSObserver type. Looking closer at that type you can see that it holds the reference to a block. Since your blocks hold strong reference to the token itself (through optional var), you have a cycle.

Try to set the optional token reference to nil once it is used:

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!)

    successToken = nil // Break reference cycle
    failureToken = nil

    self.onSuccess(self, .contentData)
}

Upvotes: 1

funct7
funct7

Reputation: 3601

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...

Upvotes: 0

Coder ACJHP
Coder ACJHP

Reputation: 2224

You need to use weak reference for selflike this :

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

successToken = nc.addObserver(
    forName: .ContentLoadSuccess,
    object: nil,
    queue: .main)
{[weak self] (_) in

    guard let strongSelf = self else { return }

    nc.removeObserver(successToken!)
    nc.removeObserver(failureToken!)

    strongSelf.onSuccess(strongSelf, .contentData)
}

failureToken = nc.addObserver(
    forName: .ContentLoadFailure,
    object: nil,
    queue: .main)
{[weak self] (_) in

    guard let strongSelf = self else { return }

    nc.removeObserver(successToken!)
    nc.removeObserver(failureToken!)

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

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

Upvotes: -3

Related Questions