SwiftMidOpen-ended
How do Timers and CADisplayLink cause memory leaks and how do you resolve them?
Explanation & Code
Answer:
Timer and CADisplayLink cause memory leaks because the target-action API causes the active RunLoop to retain the timer, and the timer retains its target object until explicitly invalidated.
Even if you dismiss a UIViewController or release a ViewModel, if an active Timer is targeting self, its deinit will never be called because the retain count never reaches zero.
Resolution Approaches:
- Weak Target Proxy: Use an intermediate forwarding object that holds a
weakreference to the real target. - Block-based Timer with
[weak self]: UseTimer.scheduledTimer(withTimeInterval:repeats:block:)and explicitly calltimer.invalidate()when done. - Swift Concurrency
Task: Replace Timer with a managedTaskutilizingTask.sleepand cancel it on cleanup.
Code Example:
// Approach 1: Weak Proxy Pattern
final class WeakTimerProxy: NSObject {
private weak var target: AnyObject?
private let action: (AnyObject) -> Void
init(target: AnyObject, action: @escaping (AnyObject) -> Void) {
self.target = target
self.action = action
super.init()
}
@objc func timerFired() {
if let target = target {
action(target)
}
}
}
// Approach 2: Modern Swift Concurrency Task
final class PollingService {
private var pollingTask: Task<Void, Never>?
func startPolling() {
pollingTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 5_000_000_000) // 5s
guard !Task.isCancelled, let self else { break }
await self.fetchLatestData()
}
}
}
func stopPolling() {
pollingTask?.cancel()
pollingTask = nil
}
deinit {
pollingTask?.cancel()
}
private func fetchLatestData() async { /* ... */ }
}
Key Points:
- Never rely on
deinitto calltimer.invalidate()if the timer strongly referencesself, becausedeinitwill never trigger - Invalidate timers in
viewWillDisappear,viewDidDisappear, or lifecycle cleanup hooks
Rate your understanding: