Hello there, my sweet fellow developers,
Several days ago I had a wonderful discussion with another “senior” dev on LinkedIn about the team management and coding practices. I requested a code sample in order to assess, if i want to spend time on him. Well, what can I say, the code was horrible. Later on down the road I will surely make a proper code review outlining all the problems and explaining, why any sane person should never work with that “senior” dev. But what especially jumped out and screamed right into my face was, how he used NSTimers. The real problem was, that he created the retain loop by using the NSTimer. That problem is really widespread and is related to people ignoring the documentation, when they think they understand the method.
For now, lets just take a look at the simplified code sample outlining the problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class TimerOwner: NSObject { var timer: Timer? deinit { self.timer?.invalidate() } override init() { super.init() self.timer = Timer.scheduledTimer( timeInterval: 0.1, target: self, selector: #selector(TimerOwner.onTimer(_:)), userInfo: nil, repeats: true ) } func onTimer(_ timer: Timer) { print("timer fired") } } |
Disclaimer: This is a sample code outlining the problem, same applies to other snippets. Pardon me, all my readers, who do actually understand that. You’d be shocked at how many people didn’t get, that my articles describe general approach, not specific use cases.
What’s the problem with this code you might ask? The entity would never be deallocated. Why? Let me cite the doc for you: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
Some of the less bright would just weakify the timer and think, that the issue is solved:
1 2 3 |
class TimerOwner: NSObject { weak var timer: Timer? ... |
Sadly, it wouldn’t solve anything, as the timer owns the target, so invalidate would never be called.
The worst, but viable approach to solve the issue would be to just invalidate the timer before releasing the entity:
1 2 3 4 5 6 7 |
class Entity { var owner: TimerOwner? = TimerOwner() deinit { self.owner?.timer?.invalidate() } } |
This solution sucks. Why? Because every time you want to get rid of the instance, you need to do something else with the instance. Moreover, in that case, if TimerOwner instance is shared between several entities, you could invalidate the timer, while someone else still uses it. In general, every time you hear or say “every time”, you should concentrate, as the code, that requires additional actions every time you do something with it is the duplication, that should be resolved.
In order to automate the process of timer invalidation, lets add the service entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class TimerResponder: NSObject { private(set) weak var target: AnyObject? fileprivate(set) weak var timer: Timer? let selector: Selector init(target: AnyObject, selector: Selector) { self.target = target self.selector = selector super.init() } func onTimer(_ timer: Timer) { let selector = self.selector if let target = self.target, true == target.responds?(to: selector) { target.perform?(selector, with: timer) } else { timer.invalidate() } } } |
This entity is a proxy between our timer target and target. Lets dissect the code line by line:
- target is the entity, that we wanted the timer selector to be fired on. It’s a weak var in order to avoid retain loops, as Timer retains its target, when added to RunLoop and releases after being invalidated. We use the AnyObject in here in order to get objc runtime method access for free without excess amounts of code.
- timer is weakified, as Timer object would store the TimerResponder, so we need that to avoid the retain loops.
- selector is the selector, that should be called on target, when timer fires.
- onTimer does the automation of timer invalidation. When the target is dead, TimerResponder invalidates the timer in order to both deallocate both Timer and TimerResponder entities. While the target is still alive, TimerResponder just performs the selector on target with timer as parameter.
You might wonder, why does the notation below work:
1 |
target.perform?(self.selector, with: timer) |
That’s one of the great features of AnyObject. It allows for simple objc runtime calls. In objc the code above would look like that:
1 2 3 |
if ([target respondsToSelector:@selector(performSelector:withObject:)]) { [target performSelector:selector withObject:timer]; } |
Ok, now the only thing we need is to use the TimerResponder with Timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
extension Timer { class func weakScheduledTimer( timeInterval: TimeInterval, target: AnyObject, selector: Selector, userInfo: Any?, repeats: Bool ) -> Timer { let responder = TimerResponder(target: target, selector: selector) let timer = Timer( timeInterval: timeInterval, target: responder, selector: #selector(TimerResponder.onTimer(_:)), userInfo: userInfo, repeats: repeats ) RunLoop.current.add(timer, forMode: .defaultRunLoopMode) return timer } } |
The extension is pretty straightforward. We just use the timer as a proxy and imitate the Timer.scheduledTimer class method, which adds the timer to the current RunLoop in the default mode.
And that’s it. If we rewrite the TimerOwner, we wouldn’t have to bother about the retain loop any more, we wouldn’t have to bother about invalidation either:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class TimerOwner: NSObject { var timer: Timer? override init() { super.init() self.timer = Timer.weakScheduledTimer( timeInterval: 0.1, target: self, selector: #selector(TimerOwner.onTimer(_:)), userInfo: nil, repeats: true ) } func onTimer(_ timer: Timer) { print("timer fired") } } |
That’s all, folks. Have a great day and stay DRY, no matter, where you are.