web-dev-qa-db-ja.com

バックグラウンドスレッドでNSTimerを作成するにはどうすればよいですか?

1秒ごとに実行する必要があるタスクがあります。現在、NSTimerが1秒ごとに繰り返し起動しています。バックグラウンドスレッド(非UIスレッド)でタイマーを起動するにはどうすればよいですか?

メインスレッドでNSTimerを起動し、NSBlockOperationを使用してバックグラウンドスレッドをディスパッチすることもできますが、もっと効率的な方法があるのではないかと考えています。

64
David

タイマーは、既に実行されているバックグラウンドスレッドで動作する実行ループにインストールする必要があります。そのスレッドは、タイマーを実際に起動させるために、実行ループの実行を継続する必要があります。そして、そのバックグラウンドスレッドが他のタイマーイベントを引き続き起動できるようにするには、とにかくイベントを実際に処理するために新しいスレッドを生成する必要があります(もちろん、実行している処理にかなりの時間がかかると仮定します)。

それが価値があるとしても、Grand Central DispatchまたはNSBlockOperationを使用して新しいスレッドを生成することでタイマーイベントを処理することは、メインスレッドの完全に合理的な使用方法だと思います。

19
Steven Fisher

ビュー(またはマップ)をスクロールするときにタイマーが実行されるようにこれが必要な場合は、異なる実行ループモードでタイマーをスケジュールする必要があります。現在のタイマーを交換します。

[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:2番目のコードブロックでは、NSTimerはメインスレッドで実行され、スクロールビューと同じ実行ループで実行されます。違いは、実行ループmodeです。明確な説明については、ブログの投稿を確認してください。

106
Marius

純粋な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
}

その後、次のようなコードを使用して、1秒のタイマーイベントを設定できます。

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秒の余裕が与えられます。必要に応じて締めることができます。

49
Brad Larson

これは動作するはずです、

NSTimersを使用せずに、バックグラウンドキューで1秒ごとにメソッドを繰り返します:)

- (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];
});

それが役に立てば幸い

15
nacho4d

Swift 3.0、

Tikhonvの答えはあまり説明していません。ここに私の理解の一部を追加します。

物事を最初に短くするために、ここにコードがあります。タイマーを作成する場所のTikhonvのコードとは[〜#〜] different [〜#〜]です。コンストラクターを使用してタイマーを作成し、ループに追加します。 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
    }
  }
}

キューを作成

まず、タイマーをバックグラウンドで実行するキューを作成し、そのタイマーをタイマーの停止に再利用するためにクラスプロパティとして保存します。開始と停止に同じキューを使用する必要があるかどうかはわかりません。これを行った理由は、警告メッセージ here が表示されたためです。

RunLoopクラスは一般にスレッドセーフとは見なされず、そのメソッドは現在のスレッドのコンテキスト内でのみ呼び出す必要があります。別のスレッドで実行されているRunLoopオブジェクトのメソッドを呼び出そうとしないでください。予期しない結果を引き起こす可能性があります。

そこで、キューを保存し、タイマーに同じキューを使用して同期の問題を回避することにしました。

また、空のタイマーを作成し、クラス変数にも保存します。タイマーを停止してnilに設定できるように、オプションにします。

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

タイマーを開始

タイマーを開始するには、まずDispatchQueueからasyncを呼び出します。次に、タイマーが既に開始しているかどうかを最初に確認することをお勧めします。タイマー変数がnilでない場合、それをinvalidate()し、nilに設定します。

次のステップは、現在のRunLoopを取得することです。作成したキューのブロックでこれを行ったため、前に作成したバックグラウンドキューのRunLoopを取得します。

タイマーを作成します。ここでは、scheduledTimerを使用する代わりに、timerのコンストラクターを呼び出して、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()を呼び出して、クラス内に保存されているタイマー変数をnilに設定します。

ここで、私は再びキューの下でそれを実行しています。ここでの警告のため、競合を避けるためにすべてのタイマー関連コードをキューの下で実行することにしました。

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)) {};

私が考えているのは、iOSで.run()を使用するだけで安全かもしれないということです。なぜなら、ドキュメントにはmacOSがインストールされ、必要に応じて追加の入力ソースが削除されて、受信者のスレッドをターゲットとするリクエストを処理することが記載されているためです。そのため、iOSは問題ないかもしれません。

13
nuynait

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()
        }
    }
}
2

Swiftのみ(ただし、Objective-Cで使用するためにおそらく変更できます)

https://github.com/arkdan/ARKExtensions からDispatchTimerをチェックアウトします。これにより、指定された時間間隔で、指定された回数(オプションで)。」

let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
    // body to execute until cancelled by timer.cancel()
}
1
user1244109

6年後の今日、私は同じことをしようとしています。ここに代替ソリューションがあります。GCDまたはNSThreadです。

タイマーは実行ループと連動して動作し、スレッドの実行ループはスレッドからのみ取得できるため、重要なのはスレッド内のスケジュールタイマーです。

メインスレッドのrunloopを除き、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はタイマーへの強い参照を持ちます。タイマーが無効になった後、ターゲットを解放します。

注:最適化として、syncDispatchQueuefunctionは、可能であれば現在のスレッドでブロックを呼び出します。実際、上記のコードをメインスレッドで実行し、タイマーはメインスレッドで起動されるため、sync関数を使用しないでください。そうしないと、タイマーが目的のスレッドで起動されません。

Xcodeで実行中のプログラムを一時停止することにより、スレッドに名前を付けてそのアクティビティを追跡できます。 GCDでは、次を使用します。

_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:サブクラススレッド

Threadサブクラスを使用する場合:

_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にタイマーを追加しないでください。そうでない場合、timerはinitの呼び出し側のスレッドのrunloopに追加され、このスレッドのrunloopには追加されません。たとえば、TimerThread timerThreadのrunloopではなく、メインスレッドのrunloopにスケジュールされます。 timerMethod()ログで確認できます。

_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()
_

PS Runloop.current.run()について、runloopを終了したい場合はこのメソッドを呼び出さないでください、run(mode: RunLoopMode, before limitDate: Date)を使用してください。実際にrun()はNSDefaultRunloopModeでこのメソッドを繰り返し呼び出します。 、モードは何ですか? runloop and thread で詳細を確認してください。

1
seedante
class BgLoop:Operation{
    func main(){
        while (!isCancelled) {
            sample();
            Thread.sleep(forTimeInterval: 1);
        }
    }
}
0
john07