Выполнить повторение NSTimer с GCD?

Мне было интересно, почему, когда вы создаете повторяющийся таймер в блоке GCD, он не работает?

Это отлично работает:

-(void)viewDidLoad{
    [super viewDidLoad];
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
}
-(void)runTimer{
    NSLog(@"hi");
}

Но эта работа:

dispatch_queue_t myQueue;

-(void)viewDidLoad{
    [super viewDidLoad];

    myQueue = dispatch_queue_create("someDescription", NULL);
    dispatch_async(myQueue, ^{
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
    });
}
-(void)runTimer{
    NSLog(@"hi");
}

Ответ 1

NSTimers запланированы в текущем потоке run loop, Однако потоки отправки GCD не имеют циклов запуска, поэтому таймеры планирования в блоке GCD ничего не сделают.

Там три разумных альтернативы:

  • Выясните, какой цикл цикла вы планируете включить таймер, и явно это сделаете. Используйте +[NSTimer timerWithTimeInterval:target:selector:userInfo:repeats:], чтобы создать таймер, а затем -[NSRunLoop addTimer:forMode:] для фактического планирования его в цикле выполнения, который вы хотите использовать. Для этого требуется наличие дескриптора в заданном цикле, но вы можете просто использовать +[NSRunLoop mainRunLoop], если хотите сделать это в основном потоке.
  • Переключитесь на использование источника отправки на основе таймера. Это реализует таймер в механизме, поддерживающем GCD, который будет запускать блок с интервалом, который вы хотите, в очереди по вашему выбору.
  • Явно dispatch_async() вернуться к основной очереди перед созданием таймера. Это эквивалентно опции № 1, использующей цикл основного запуска (так как он также создает таймер в основном потоке).

Конечно, здесь возникает реальный вопрос: зачем вы создаете таймер из очереди GCD, чтобы начать?

Ответ 2

NSTimer запланирован для потоков runloop. В этом вопросе runloop потока, отправленного GCD, не запущен. Вы должны запустить его вручную, и должен быть выход из цикла выполнения, поэтому вы должны сохранить ссылку на NSTimer и сделать это недействительным в соответствующее время.

NSTimer имеет сильную ссылку на цель, поэтому цель не может иметь сильную ссылку на таймер, а runloop имеет сильную ссылку на таймер.

weak var weakTimer: Timer?
func configurateTimerInBackgroundThread(){
    DispatchQueue.global().async {
        // Pause program execution in Xcode, you will find thread with this name
        Thread.current.name = "BackgroundThreadWithTimer"
        // This timer is scheduled to current run loop
        self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
        // Start current runloop manually, otherwise NSTimer won't fire.
        RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
    }
}

@objc func runTimer(){
    NSLog("Timer is running in mainThread: \(Thread.isMainThread)")
}

Если таймер недействителен в будущем, приостановите выполнение программы еще раз в Xcode, вы увидите, что поток исчез.

Конечно, потоки, отправленные GCD, имеют runloop. GCD генерирует и повторно использует потоки внутри, потоки анонимны для вызывающего. Если вы не чувствуете себя в безопасности, вы можете использовать Thread. Не бойтесь, код очень прост.

На самом деле, я пробую то же самое на прошлой неделе и получаю то же самое с апеллятором, тогда я нашел эту страницу. Я пробую NSThread, прежде чем сдаться. Оно работает. Так почему NSTimer в GCD не может работать? Должен быть. Прочтите документ runloop, чтобы узнать, как работает NSTimer.

Используйте NSThread для работы с NSTimer:

func configurateTimerInBackgroundThread(){
    let thread = Thread.init(target: self, selector: #selector(addTimerInBackground), object: nil)
    thread.name = "BackgroundThreadWithTimer"
    thread.start()
}

@objc func addTimerInBackground() {
    self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
    RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}

Ответ 3

Это плохая идея. Я собирался удалить этот ответ, но я оставил его здесь, чтобы другие не делали ту же ошибку, что и я. Спасибо #Kevin_Ballard за указание на это.

Вы добавили бы только одну строку в свой пример, и она будет работать так же, как вы ее написали:

[[NSRunLoop currentRunLoop] run]

чтобы вы получили:

    -(void)viewDidLoad{
        [super viewDidLoad];

        myQueue = dispatch_queue_create("someDescription", NULL);
        dispatch_async(myQueue, ^{
            [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] run]
        });
    }

Так как ваша очередь myQueue содержит NSThread и содержит NSRunLoop, и поскольку код в dispatch_async запущен, контекст этого NSThread, currentRunLoop будет возвращать цикл остановленного запуска, связанный с потоком вашего очереди.