Как создать NSTimer в фоновом потоке?
У меня есть задача, которую нужно выполнять каждые 1 секунду. В настоящее время у меня есть NSTimer стрельбы постоянно раз в 1 сек. Как у меня есть огонь, таймер в фоновом потоке (не в UI-потоке)?
Я мог бы иметь огонь NSTimer в основном потоке, а затем использовать NSBlockOperation для отправки фонового потока, но мне интересно, есть ли более эффективный способ сделать это.
10 ответов:
таймер должен быть установлен в цикл выполнения, работающий на уже запущенном фоновом потоке. Этот поток должен был бы продолжать запускать цикл запуска, чтобы таймер действительно срабатывал. И для того, чтобы этот фоновый поток мог продолжать запускать другие события таймера, ему нужно было бы создать новый поток, чтобы фактически обрабатывать события в любом случае (предполагая, конечно, что обработка, которую вы делаете, занимает значительное количество времени).
для чего бы это ни стоило, я думаю обработка событий таймера путем создания нового потока с помощью Grand Central Dispatch или
NSBlockOperationвполне разумное использование вашего основного потока.
Если вам это нужно, чтобы таймеры все еще работали при прокрутке ваших представлений (или карт), вам нужно запланировать их в другом режиме цикла запуска. Замените текущий таймер:
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];С этим:
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];для получения дополнительной информации, проверьте это сообщение в блоге:отслеживание событий останавливает NSTimer
EDIT: второй блок кода, NSTimer все еще работает в основном потоке, все еще в том же цикле запуска, что и scrollviews. Разница заключается в прогоне петля режим. Проверьте сообщение в блоге для четкого объяснения.
Если вы хотите перейти на чистый GCD и использовать источник отправки, у Apple есть пример кода для этого в их Руководство По Программированию Параллелизма:
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 }затем вы можете настроить односекундное событие таймера, используя следующий код:
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 });убедитесь, что сохранить и отпустить таймер, когда закончите, конечно. Вышеизложенное дает вам 1/10-секундную свободу действий при стрельбе из этих событий, которые вы может затянуть, если вы хотите.
Это должно работать,
он повторяет метод каждые 1 секунду в фоновой очереди без использования 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]; }); }Если вы находитесь в основной очереди, и вы хотите вызвать выше метод, вы можете сделать это, чтобы он изменился на фоновую очередь перед запуском :)
dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(q_background, ^{ [self methodToRepeatEveryOneSecond]; });надеюсь, что это помогает
для swift 3.0,
ответ Тихонва не объясняет слишком много. Здесь добавляется часть моего понимания.
коротко во-первых, вот код. Это разные из кода Тихоньва в том месте, где я создаю таймер. Я создаю таймер с помощью конструктора и добавляю его в цикл. Я думаю, что функция scheduleTimer добавит таймер в RunLoop основного потока. Так что лучше создать таймер с помощью конструктор.
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 } } }Создать Очередь
во-первых, создайте очередь для запуска таймера в фоновом режиме и сохраните эту очередь как свойство класса, чтобы повторно использовать ее для таймера остановки. Я не уверен, что нам нужно использовать одну и ту же очередь для запуска и остановки, причина, по которой я это сделал, заключается в том, что я увидел предупреждающее сообщение здесь.
класс RunLoop, как правило, не считаются потокобезопасными и его методы должны вызываться только в пределах контекст текущего нитка. Вы никогда не должны пытаться вызвать методы объекта RunLoop запущена в другом потоке, так как это может привести к неожиданным результаты.
поэтому я решил сохранить очередь и использовать ту же очередь для таймера, чтобы избежать проблем синхронизации.
также создать пустой таймер и хранится в переменной класса. Сделайте его необязательным, чтобы вы могли остановить таймер и установить его на ноль.
class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent) let timer: Timer? }старт Таймер
чтобы запустить таймер, сначала вызовите async из DispatchQueue. Тогда это хорошая практика, чтобы сначала проверить, если таймер уже запущен. Если переменная timer не равна nil, то invalidate() IT и установите ее в nil.
следующим шагом является получение текущего RunLoop. Поскольку мы сделали это в блоке очереди, который мы создали, он получит RunLoop для фоновой очереди, которую мы создали раньше.
создать таймер. Вот вместо того, чтобы использовать scheduledTimer, мы просто называем конструктор таймера и передать все, что вы хотите для таймера, такие как timeInterval, цель, селектор и т. д.
добавьте созданный таймер в RunLoop. Запустить его.
вот вопрос о запуске RunLoop. Согласно документации здесь, он говорит, что он эффективно начинает бесконечный цикл, который обрабатывает данные из входных источников и таймеров цикла запуска.
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() { // under queue by default debug() } func debug() { let name = __dispatch_queue_get_label(nil) print(String(cString: name, encoding: .utf8)) }функция отладки выше используется для печати имени очереди. Если вы когда-нибудь беспокоиться, если он был запущен в очереди, вы можете позвонить ему, чтобы проверить.
Стоп Таймер
остановить таймер легко, вызовите validate () и установите переменную таймера, хранящуюся внутри класса, на ноль.
здесь я снова запускаю его под очередью. Из-за предупреждения здесь я решил запустить все связанный с таймером код под очередью, чтобы избежать конфликтов.
func stopTimer() { queue.sync { [unowned self] in guard let _ = self.timer else { // error, timer already stopped return } self.timer?.invalidate() self.timer = nil } }вопросы, связанные с RunLoop
Я как-то немного запутался, если нам нужно вручную остановить RunLoop или нет. Согласно документации здесь, кажется, что когда к нему не прикреплены таймеры, то он сразу же выйдет. Поэтому, когда мы останавливаем таймер, он должен существовать сам по себе. Однако, в конце этого документа, он также сказал:
удаление всех известных источников входного сигнала и таймеры из цикла выполнения не являются гарантируйте, что цикл выполнения завершится. macOS можно устанавливать и удалять дополнительные источники ввода по мере необходимости для обработки запросов, направленных на нить приемника. Поэтому эти источники могут предотвратить цикл выполнения от выхода.
я попробовал Решение ниже, что предусмотрено в документации для гарантии завершения цикла. Однако таймер не срабатывает после того, как я меняю .запустите () в код ниже.
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};что Я думаю, что это может быть безопасным для использования .run () на iOS. Поскольку в документации указано, что macOS устанавливает и удаляет дополнительные источники ввода по мере необходимости для обработки запросов, направленных на поток получателя. Так что iOS может быть в порядке.
мое решение Swift 3.0 для iOS 10+,
timerMethod()вызывается в фоновом потоке.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() } } }
только Swift (хотя, вероятно, может быть изменен для использования с Objective-C)
проверить
DispatchTimerот https://github.com/arkdan/ARKExtensions, который " выполняет закрытие в указанной очереди отправки с заданными интервалами времени для заданного количества раз (необязательно). "let queue = DispatchQueue(label: "ArbitraryQueue") let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in // body to execute until cancelled by timer.cancel() }
сегодня после 6 лет я пытаюсь сделать то же самое, вот альтернативное решение: GCD или NSThread.
таймеры работают в сочетании с циклами запуска, runloop потока можно получить только из потока, поэтому ключом является таймер расписания в потоке.
кроме runloop основного потока, runloop должен запускаться вручную; должны быть некоторые события для обработки в running runloop, такие как таймер, иначе runloop выйдет, и мы можем использовать это для выхода из runloop, если таймер является только источник события: аннулировать таймер.
следующий код Swift 4:
решение 0: GCD
weak var weakTimer: Timer? @objc func timerMethod() { // vefiry whether timer is fired in background thread NSLog("It's called from main thread: \(Thread.isMainThread)") } func scheduleTimerInBackgroundThread(){ DispatchQueue.global().async(execute: { //This method schedules timer to current runloop. self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true) //start runloop manually, otherwise timer won't fire //add timer before run, otherwise runloop find there's nothing to do and exit directly. RunLoop.current.run() }) }таймер имеет сильную ссылку на цель, а runloop имеет сильную ссылку на таймер, после того, как таймер недействителен, он освобождает цель, поэтому держите слабую ссылку на нее в цели и аннулируйте ее в соответствующее время, чтобы выйти из runloop(а затем выйти из потока).
Примечание: в качестве оптимизации,
syncфункцииDispatchQueueвызывает блок на текущий поток, когда это возможно. На самом деле, вы выполняете выше код в основном потоке, таймер запускается в основном потоке, так что не используйтеsyncфункция, в противном случае таймер не срабатывает на поток, который вы хотите.вы можете назвать поток для отслеживания его активности, приостановив выполнение программы в Xcode. В НОД, использовать:
Thread.current.name = "ThreadWithTimer"Решение 1: Нить
мы могли бы использовать NSThread напрямую. Не бойтесь, код легко.
func configurateTimerInBackgroundThread(){ // Don't worry, thread won't be recycled after this method return. // Of course, it must be started. let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil) thread.start() } @objc func addTimer() { weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true) RunLoop.current.run() }Решение 2: Подкласс Нить
если вы хотите использовать подкласс потока:
class TimerThread: Thread { var timer: Timer init(timer: Timer) { self.timer = timer super.init() } override func main() { RunLoop.current.add(timer, forMode: .defaultRunLoopMode) RunLoop.current.run() } }Примечание: не добавляйте таймер в init, в противном случае таймер добавляется в runloop вызывающего потока init, а не в runloop этого потока, например, вы запускаете следующий код в основном потоке, если
TimerThreadдобавить таймер в метод init, таймер будет запланирован на runloop основного потока, а не runloop timerThread. Вы можете проверить это вtimerMethod()log.let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true) weakTimer = timer let timerThread = TimerThread.init(timer: timer) timerThread.start()P. S О
Runloop.current.run(), его документ предлагает не вызывать этот метод если мы хотим завершить runloop, используйтеrun(mode: RunLoopMode, before limitDate: Date), на самом делеrun()повторно вызвать этот метод в NSDefaultRunloopMode, что такое режим? Подробнее в runloop и thread.
class BgLoop:Operation{ func main(){ while (!isCancelled) { sample(); Thread.sleep(forTimeInterval: 1); } } }
Если вы хотите, чтобы ваш NSTimer, чтобы работать в фоновом режиме, выполните следующие действия-
- вызов [self beginBackgroundTask] метод в applicationWillResignActive методы
- вызов [self endBackgroundTask] метод в applicationWillEnterForeground
вот это
-(void)beginBackgroundTask { bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endBackgroundTask]; }]; } -(void)endBackgroundTask { [[UIApplication sharedApplication] endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }
Comments