1秒ごとに実行する必要があるタスクがあります。現在、NSTimer が 1 秒ごとに繰り返し起動しています。バックグラウンド スレッド (非 UI スレッド) でタイマーを起動するにはどうすればよいですか?
メイン スレッドで NSTimer を起動し、NSBlockOperation を使用してバックグラウンド スレッドをディスパッチすることもできますが、これを行うためのより効率的な方法があるかどうか疑問に思っています。
1秒ごとに実行する必要があるタスクがあります。現在、NSTimer が 1 秒ごとに繰り返し起動しています。バックグラウンド スレッド (非 UI スレッド) でタイマーを起動するにはどうすればよいですか?
メイン スレッドで NSTimer を起動し、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 : コードの 2 番目のブロック、NSTimer は引き続きメイン スレッドで実行され、スクロールビューと同じ実行ループで実行されます。違いは実行ループモードです。明確な説明については、ブログ投稿を確認してください。
純粋な GCD を使用してディスパッチ ソースを使用する場合は、Apple のConcurrency Programming Guideにサンプル コードがあります。
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;
}
スウィフト 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 秒の余裕が与えられます。必要に応じて、これを引き締めることができます。
タイマーは、既に実行中のバックグラウンド スレッドで動作する実行ループにインストールする必要があります。そのスレッドは、タイマーを実際に起動させるために実行ループを実行し続ける必要があります。そして、そのバックグラウンド スレッドが引き続き他のタイマー イベントを起動できるようにするには、実際にイベントを処理する新しいスレッドを生成する必要があります (もちろん、実行中の処理にかなりの時間がかかると仮定します)。
価値が何であれ、グランドセントラルディスパッチを使用して新しいスレッドを生成することによってタイマーイベントを処理するかNSBlockOperation
、メインスレッドを完全に合理的に使用すると思います。
これはうまくいくはずです、
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];
});
それが役に立てば幸い
Tikhonvの答えはあまり説明していません。ここに私の理解の一部を追加します。
最初に簡潔にするために、コードを次に示します。タイマーを作成する場所の Tikhonv のコードとは異なります。コンストラクターを使用してタイマーを作成し、ループに追加します。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 を手動で停止する必要があるかどうかについて、少し混乱しています。ドキュメント here によると、タイマーが接続されていない場合は、すぐに終了するようです。したがって、タイマーを停止すると、それ自体が存在するはずです。ただし、その文書の最後には、次のようにも記載されています。
実行ループからすべての既知の入力ソースとタイマーを削除しても、実行ループが終了する保証はありません。macOS は、必要に応じて追加の入力ソースをインストールおよび削除して、受信者のスレッドを対象とする要求を処理できます。したがって、これらのソースは実行ループの終了を妨げる可能性があります。
ループを終了することを保証するために、ドキュメントで提供されている以下のソリューションを試しました。ただし、 .run() を以下のコードに変更した後、タイマーは起動しません。
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};
私が考えているのは、iOS で .run() を使用するだけでも安全かもしれないということです。ドキュメントには、受信者のスレッドを対象とした要求を処理するために、必要に応じて macOS が追加の入力ソースをインストールおよび削除すると記載されているためです。だからiOSは大丈夫かもしれません。
iOS 10+ 用の私の Swift 3.0 ソリューションは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()
}
}
}