How to create NSTimer in a background thread?

I have a task that must be performed every 1 second. I am currently running NSTimer every 1 second. How do I have a timer fire in a background thread (not a UI thread)?

I could start NSTimer in the main thread and then use NSBlockOperation to send the background thread, but I am wondering if there is a more efficient way to do this.

+58
objective-c cocoa nstimer nsrunloop nsblockoperation
Nov 29 '11 at 1:25
source share
7 answers

The timer must be set to a run loop running on an already running background thread. This thread will have to continue to run the run loop to start the timer. And for this background thread, in order to continue the possibility of triggering other timer events, it will need to create a new thread to actually process the events anyway (assuming, of course, that the processing you process takes a considerable amount of time).

Whatever the cost, I think that handling timer events, creating a new thread using Grand Central Dispatch or NSBlockOperation , NSBlockOperation good use of your main thread.

+16
Nov 29 2018-11-11T00:
source share

If you need the timers to still run when scrolling through your views (or maps), you need to schedule them in different modes of the run loop. Replace the current timer:

 [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; 

With the help of this:

 NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

See this blog post for more details: Event Tracking Stops NSTimer

EDIT: The second block of code, NSTimer, still runs in the main thread, still in the same run loop as scrollviews. The difference is the cycle mode . Check out the blog post for a clear explanation.

+93
Aug 29 '12 at 8:39
source share

If you want to switch to a pure GCD and use a dispatch source, Apple has a sample code for this in the Concurrency Programming Guide :

 dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block) { dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); if (timer) { dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway); dispatch_source_set_event_handler(timer, block); dispatch_resume(timer); } return timer; } 

Swift 3:

 func createDispatchTimer(interval: DispatchTimeInterval, leeway: DispatchTimeInterval, queue: DispatchQueue, block: @escaping ()->()) -> DispatchSourceTimer { let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0), queue: queue) timer.scheduleRepeating(deadline: DispatchTime.now(), interval: interval, leeway: leeway) // Use DispatchWorkItem for compatibility with iOS 9. Since iOS 10 you can use DispatchSourceHandler let workItem = DispatchWorkItem(block: block) timer.setEventHandler(handler: workItem) timer.resume() return timer } 

You can then set up a one-second timer event using the following code:

 dispatch_source_t newTimer = CreateDispatchTimer(1ull * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Repeating task }); 

make sure you save and release your timer when done, of course. The above gives you a 1/10-second freedom of action when shooting these events, which you could tighten if you want.

+44
Dec 06 '11 at 17:00
source share

That should work

It repeats the method every 1 second in the background without using NSTimers :)

 - (void)methodToRepeatEveryOneSecond { // Do your thing here // Call this method again using GCD dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); dispatch_after(popTime, q_background, ^(void){ [self methodToRepeatEveryOneSecond]; }); } 

If you are in the main queue and want to call the method above, you can do this so that it changes in the background before starting :)

 dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(q_background, ^{ [self methodToRepeatEveryOneSecond]; }); 

Hope this helps

+15
Nov 29 2018-11-11T00:
source share

For quick 3.0,

Tikhonva's answer does not explain too much. Something from my understanding is added here.

To make things short, here is the code. This is DIFFERENT from Tikhonov's code in the place where I create the timer. I create a timer using the constructor and add it to the loop. I think the schedTimer function will add a timer to the main RunLoop thread. Therefore, it is better to create a timer using the constructor.

 class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent) let timer: Timer? private func startTimer() { // schedule timer on background queue.async { [unowned self] in if let _ = self.timer { self.timer?.invalidate() self.timer = nil } let currentRunLoop = RunLoop.current self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true) currentRunLoop.add(self.timer!, forMode: .commonModes) currentRunLoop.run() } } func timerTriggered() { // it will run under queue by default debug() } func debug() { // print out the name of current queue let name = __dispatch_queue_get_label(nil) print(String(cString: name, encoding: .utf8)) } func stopTimer() { queue.sync { [unowned self] in guard let _ = self.timer else { // error, timer already stopped return } self.timer?.invalidate() self.timer = nil } } } 

Create queue

First create a queue to start the timer in the background and save this queue as a class property to reuse it for the stop timer. I'm not sure that we need to use the same queue to start and stop, so I did this because I saw a warning message here here .

The RunLoop class is usually not considered thread safe and its methods should only be called in the context of the current thread. You should never try to call methods on an object. RunLoop is running in another thread, as this may lead to unexpected results.

Therefore, I decided to save the queue and use the same queue for the timer to avoid synchronization problems.

Also create an empty timer and save it in a class variable. Make this optional to stop the timer and set it to zero.

 class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent) let timer: Timer? } 

Start timer

To start the timer, first call async from the DispatchQueue. Then it’s better to check if the timer is already running. If the timer variable is not nil, then invalidate () and set it to zero.

The next step is to get the current RunLoop. Since we did this in the created queue block, it will get RunLoop for the previously created background queue.

Create a timer. Here, instead of using schedTimer, we simply call the timer constructor and pass in any property that you want for the timer, for example timeInterval, target, selector, etc.

Add the created timer to RunLoop. Run it.

Here is a question about running RunLoop. According to the documentation here it says that it actually starts an infinite loop that processes data from input sources and timers.

 private func startTimer() { // schedule timer on background queue.async { [unowned self] in if let _ = self.timer { self.timer?.invalidate() self.timer = nil } let currentRunLoop = RunLoop.current self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true) currentRunLoop.add(self.timer!, forMode: .commonModes) currentRunLoop.run() } } 

Trigger timer

Implement the function as usual. When this function is called, it is called in the default queue.

 func timerTriggered() { // under queue by default debug() } func debug() { let name = __dispatch_queue_get_label(nil) print(String(cString: name, encoding: .utf8)) } 

The debugging function above is used to print the queue name. If you ever worried if he was running in line, you can call him to check.

Stop timer

The break timer is simple, call validate () and set the timer variable stored inside the class to zero.

Here I run it again under the queue. Due to a warning here, I decided to run all the timer related code in the queue to avoid conflicts.

 func stopTimer() { queue.sync { [unowned self] in guard let _ = self.timer else { // error, timer already stopped return } self.timer?.invalidate() self.timer = nil } } 

Questions related to RunLoop

I'm a little bit confused if we need to manually stop RunLoop or not. According to the documentation here, it seems that when timers are not connected to it, it will exit immediately. Therefore, when we stop the timer, it must exist by itself. However, at the end of this document, he also said:

Removing all known input sources and timers from the startup cycle is not a sure that the execution cycle is complete. macOS can install and remove additional input sources needed to process thread-oriented requests. Thus, these sources can interfere with the start-up cycle from the output.

I tried the solution below provided in the documentation to guarantee the completion of the cycle. However, the timer does not fire after I changed .run () to the code below.

 while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {}; 

I think it can only be safe to use .run () on iOS. Since the documentation states that macOS installs and removes additional input sources as needed to handle requests aimed at the recipient stream. So iOS might be fine.

+9
Apr 19 '17 at 18:57
source share

My Swift 3.0 solution for iOS 10+, timerMethod() will be called in the background.

 class ViewController: UIViewController { var timer: Timer! let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil) override func viewDidLoad() { super.viewDidLoad() queue.async { [unowned self] in let currentRunLoop = RunLoop.current let timeInterval = 1.0 self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerMethod), userInfo: nil, repeats: true) self.timer.tolerance = timeInterval * 0.1 currentRunLoop.add(self.timer, forMode: .commonModes) currentRunLoop.run() } } func timerMethod() { print("code") } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) queue.sync { timer.invalidate() } } } 
+2
Feb 28 '17 at 12:22
source share

Swift only (although you can probably change it for use with Objective-C)

Check DispatchTimer from https://github.com/arkdan/ARKExtensions , which "Closes the specified dispatch queue at the specified time intervals for the specified number of times (optional).

 let queue = DispatchQueue(label: "ArbitraryQueue") let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in // body to execute until cancelled by timer.cancel() } 
+1
Feb 02 '17 at 11:49
source share



All Articles