Timer On Background Thread
Sometimes when we schedule a timer we might want to schedule it on a background thread if the timer is updating in a short interval. Here is how I managed to do it.
NSTimer and RunLoop
Usually, when you schedule a timer, you call scheduledTimer:withTimeInterval. When this get called, it actually gets the main thread RunLoop and add into it. However, for background threads, you have to get the RunLoop and add NSTimer onto the RunLoop yourself.
Codes
Here is the full block of codes
1 | class RunTimer{ |
Create Queue
First, create a queue to make timer run on background and store that queue as a class property in order to reuse it for stop timer. I am not sure if we need to use the same queue for start and stop, the reason I did this is because I saw a warning message here.
The RunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an RunLoop object running in a different thread, as doing so might cause unexpected results.
So I decided to store the queue and use the same queue for the timer to avoid synchronization issues.
Also create an empty timer and stored in the class variable as well. Make it optional so you can stop the timer and set it to nil.
1 | class RunTimer{ |
Start Timer
To start timer, first call async from DispatchQueue. Then it is a good practice to first check if the timer has already started. If the timer variable is not nil, then invalidate() it and set it to nil.
The next step is to get the current RunLoop. Because we did this in the block of queue we created, it will get the RunLoop for the background queue we created before.
Create the timer. Here instead of using scheduledTimer, we just call the constructor of timer and pass in whatever property you want for the timer such as timeInterval, target, selector, etc.
Add the created timer to the RunLoop. Run it.
Here is a question about running the RunLoop. According to the documentation here, it says it effectively begins an infinite loop that processes data from the run loop's input sources and timers.
1 | private func startTimer() { |
Trigger Timer
Implement the function as normal. When that function gets called, it is called under the queue by default.
1 | func timerTriggered() { |
The debug function above is use to print out the name of the queue. If you ever worry if it has been running on the queue, you can call it to check.
Stop Timer
Stop timer is easy, call validate() and set the timer variable stored inside class to nil.
Here I am running it under the queue again. Because of the warning here I decided to run all the timer related code under the queue to avoid conflicts.
1 | func stopTimer() { |
Some Additional Questions
I am not fully understand this concepts. Here is some questions I have.
Stop RunLoop
I am somehow a little bit confused on if we need to manually stop the RunLoop or not. According to the documentation here, it seems that when no timers attached to it, then it will exits immediately. So when we stop the timer, it should exists itself. However, at the end of that document, it also said that removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
I tried the solution below which provide in the documentation for guarantee to terminate the loop. However the timer does not fired after I change .run() to the code below.
1 | while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {}; |
What I am thinking is that it might be safe for just using .run() on iOS. Because the documentation states that macOS is install and remove additional input sources as needed to process requests targeted at the receiver’s thread. So iOS might be fine.
RunLoop modes
When add timer onto RunLoop, we need to pass a variable for the mode. Table 3-1 here list all the modes available and their definition but I didn't understand them all.
My understanding right now is if it is a timer, it will use .commonModes.
I may update this part whenever I got time to look deep into this.
Reference
Here are some useful links about running timer on background threads
