Why does a Timer keep your object alive, and how do you stop it?
Predict first
What does this print with a strong capture?
import Foundation
final class Poller {
var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in self.tick() }
}
func tick() {}
deinit { print("deinit") }
}
var p: Poller? = Poller()
p = nil
print("after")
- deinit, after
- after
Predict first
What does this print with a weak capture and invalidation in deinit?
import Foundation
final class Poller {
var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in self?.tick() }
}
func tick() {}
deinit { timer?.invalidate(); print("deinit") }
}
var p: Poller? = Poller()
p = nil
print("after")
- deinit, after
- after
Mental model. An alarm clock in a hotel lobby, set to ring in your name. The hotel keeps your room reserved until someone switches it off.
A scheduled Timer is retained by the run loop until it is invalidated, and the timer retains whatever its callback captures. A repeating timer whose block uses self therefore keeps self alive for as long as the timer runs: the run loop is the owner you did not notice. The old target and selector API retains its target in the same way.
Two fixes work together. Capture [weak self] in the block so the timer does not own you, and invalidate the timer when you are done. Be careful where: deinit only runs if nothing holds the object, so with a strong capture, invalidating in deinit is too late because it never fires. Invalidate from an explicit lifecycle point (viewWillDisappear, stop()), and use a weak-capturing block or a weak proxy for CADisplayLink, which retains its target.
The run loop holds the timer, the timer’s block holds the poller, and the poller holds the timer. Nothing frees it.
The timer no longer owns the poller, so it frees as soon as the variable is cleared, and deinit now gets the chance to invalidate the timer.
Common traps
- Invalidating in
deinitand capturing strongly. The deinit that would stop the timer is the one the timer prevents. - Using
scheduledTimer(timeInterval:target:selector:)and thinkingweakhelps. The timer retains the target regardless.
They might follow up with
- When is a timer released? After
invalidate(), or after a one-shot timer fires. - What about
CADisplayLink? It retains its target too. Use a small weak-forwarding proxy object as the target.
Related questions
- 3.3.3 When does a closure create a cycle, and why doesn’t a lazy var closure always?
- 2.4.2 How do you avoid leaks and crashes with notification observers?
← When does a closure create a cycle, and why doesn’t a lazy var… How do Combine subscriptions create cycles, and why is assign(to:on:)… →