Reputation: 3601
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
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
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:
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
Reputation: 2224
You need to use weak reference for self
like 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