How do you avoid leaks and crashes with notification observers?
Predict first
What does this print?
final class Screen {
var token: NSObjectProtocol?
init(_ nc: NotificationCenter) {
token = nc.addObserver(forName: .init("x"), object: nil, queue: nil) { _ in self.handle() }
}
func handle() {}
deinit { print("deinit") }
}
let nc = NotificationCenter()
do { _ = Screen(nc) }
print("end")
- deinit, end
- end
Predict first
What does this print after adding [weak self]?
final class Screen {
var token: NSObjectProtocol?
init(_ nc: NotificationCenter) {
token = nc.addObserver(forName: .init("x"), object: nil, queue: nil) { [weak self] _ in self?.handle() }
}
func handle() {}
deinit { print("deinit") }
}
let nc = NotificationCenter()
do { _ = Screen(nc) }
print("end")
- deinit, end
- end
Mental model. The token is the cancellation slip for a newspaper subscription. If you never hand it in, the paper keeps arriving and the house cannot be demolished.
The block-based addObserver(forName:object:queue:using:) returns an opaque token. The center keeps your block alive until you call removeObserver(token), and the block keeps whatever it captures alive. If the block captures self and self stores the token, you have a cycle through the center, and the object never deallocates.
Capture [weak self] in the block and remove the token in deinit (or hold it in a small helper that does). Selector-based observers on NSObject subclasses are removed automatically on modern systems, but the block API is not. Also remember a leaked observer keeps firing, so you may see handlers run for screens that are long gone.
The center holds the block, the block holds self. Nothing can release it, so deinit never runs.
With a weak capture nothing keeps the screen alive, so it deallocates when the scope ends. (The leftover registration is harmless but should still be removed.)
Common traps
- Strong
selfin the observer block. It forms a retain cycle via the notification center. - Dropping the token. You can no longer remove that observer.
They might follow up with
- How do you debug a leak like this? Check
deinitprints, then Debug Memory Graph for the object held by the notification center. - What is a cleaner modern option?
for await _ in center.notifications(named:)in a task you cancel, or a Combinepublisher(for:)with itsAnyCancellable.
Related questions
- 2.4.1 How does NotificationCenter deliver a notification, and on which thread?
- 2.4.3 How do you pass data in a notification safely?
- 1.5.4 What does a capture list like [weak self], [unowned self], [self], or [foo] actually do?
← How does NotificationCenter deliver a notification, and on which… How do you pass data in a notification safely? →