web-dev-qa-db-ja.com

GCDでNSTimerを繰り返し実行しますか?

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");
}
19
Shredder2794

NSTimers は現在のスレッドの 実行ループ でスケジュールされます。ただし、GCDディスパッチスレッドには実行ループがないため、GCDブロックでタイマーをスケジュールしても何も起こりません。

3つの合理的な選択肢があります。

  1. タイマーをスケジュールする実行ループを見つけて、明示的に実行します。 _+[NSTimer timerWithTimeInterval:target:selector:userInfo:repeats:]_ を使用してタイマーを作成し、次に _-[NSRunLoop addTimer:forMode:]_ を使用して、使用する実行ループで実際にタイマーをスケジュールします。これには、問題の実行ループにハンドルが必要ですが、メインスレッドで実行する場合は、 _+[NSRunLoop mainRunLoop]_ を使用できます。
  2. タイマーベースの使用に切り替えます ディスパッチソース 。これにより、GCD対応メカニズムにタイマーが実装され、選択したキューで必要な間隔でブロックが実行されます。
  3. 明示的に dispatch_async() タイマーを作成する前にメインキューに戻ります。これは、メイン実行ループを使用するオプション#1と同等です(メインスレッドにタイマーも作成されるため)。

もちろん、ここでの本当の問題は、なぜ最初にGCDキューからタイマーを作成するのかということです。

59
Lily Ballard

NSTimerは、スレッドのrunloopにスケジュールされています。問題のコードでは、GCDによってディスパッチされたスレッドのrunloopが実行されていません。手動で開始する必要があり、実行ループを終了する方法が必要なので、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は内部でスレッドを生成して再利用し、スレッドは呼び出し元に対して匿名です。安全に感じられない場合は、スレッドを使用できます。恐れることはありません、コードはとても簡単です。

実際、私は先週同じことを試みて、askerで同じ失敗をしました、そして私はこのページを見つけました。諦める前にNSThreadを試してみます。できます。では、なぜGCDのNSTimerが機能しないのでしょうか。そのはず。 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)
}
0
seedante