Как создать NSTimer в фоновом потоке?
У меня есть задача, которая должна выполняться каждые 1 секунду. В настоящее время я запускаю NSTimer каждые 1 сек. Как у меня есть огонь таймера в фоновом потоке (не UI-поток)?
Я мог бы запустить NSTimer в основном потоке, а затем использовать NSBlockOperation для отправки фонового потока, но мне интересно, есть ли более эффективный способ сделать это.
Ответы
Ответ 1
Таймер должен быть установлен в цикл запуска, работающий на уже запущенном фоновом потоке. Этот поток должен будет продолжать запускать цикл выполнения, чтобы запустить таймер. И для этого фонового потока, чтобы продолжить возможность запуска других событий таймера, ему нужно будет создать новый поток, чтобы фактически обрабатывать события в любом случае (предполагая, конечно, что обрабатываемая вами обработка занимает значительное количество времени).
Что бы это ни стоило, я думаю, что обработка событий таймера, создавая новый поток с помощью Grand Central Dispatch или NSBlockOperation
, вполне разумно использует ваш основной поток.
Ответ 2
Если вам нужно, чтобы таймеры все еще выполнялись при прокрутке ваших представлений (или карт), вам нужно запланировать их в разных режимах цикла прогона. Замените текущий таймер:
[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
РЕДАКТИРОВАТЬ:
второй блок кода, NSTimer по-прежнему работает в основном потоке, все еще в том же цикле запуска, что и scrollviews. Разница заключается в режиме цикла . Проверьте сообщение в блоге, чтобы получить четкое объяснение.
Ответ 3
Если вы хотите перейти на чистый GCD и использовать источник отправки, у Apple есть пример кода для этого в Concurrency Руководство по программированию:
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-секундную свободу действий при стрельбе этих событий, которые вы могли бы затянуть, если хотите.
Ответ 4
Это должно работать,
Он повторяет метод каждые 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];
});
Надеюсь, что это поможет
Ответ 5
Для быстрого 3.0,
Ответ Тихонва не слишком объясняет. Здесь добавляется кое-что из моего понимания.
Чтобы сделать вещи короткими, вот код. Это DIFFERENT из кода Тихонова в том месте, где я создаю таймер. Я создаю таймер с помощью конструктора и добавляю его в цикл. Я думаю, что функция schedTimer добавит таймер в основной поток 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. Тогда лучше проверить, действительно ли таймер уже запущен. Если переменная таймера не равна nil, тогда invalidate() и установите ее на нуль.
Следующий шаг - получить текущий RunLoop. Поскольку мы сделали это в созданном блоке очереди, он получит RunLoop для ранее созданной очереди фона.
Создайте таймер. Здесь вместо использования schedTimer мы просто вызываем конструктор таймера и передаем любое свойство, которое вы хотите для таймера, например timeInterval, target, selector и т.д.
Добавьте созданный таймер в 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 может устанавливать и удалять дополнительные источники входных данных, необходимые для обработки запросов, ориентированных на приемники нить. Таким образом, эти источники могут препятствовать циклу запуска от выхода.
Я попробовал решение ниже, приведенное в документации для гарантии завершения цикла. Тем не менее, таймер не срабатывает после того, как я изменил .run() на код ниже.
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};
Я думаю, что это может быть безопасно только для использования .run() в iOS. Поскольку в документации указано, что macOS устанавливает и удаляет дополнительные источники входных данных по мере необходимости для обработки запросов, нацеленных на поток получателя. Таким образом, iOS может быть в порядке.
Ответ 6
Решение My 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()
}
}
}
Ответ 7
Только 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()
}