NSRunLoopを立ち上げるベストプラクティス
問題
NSRunLoopは個人的に以下の2点の仕様によって, 罠に嵌まることが多いように思う.
- runModeが条件を満たしていないとイベントが発火しない
- timerなどを紐付けたRunLoopが終了すると, 以降のイベントは発火しない
その為, View関係の処理を登録したい場合はmainRoopに, そうでない場合は自分でrunLoopを立ち上げることになると思う.
その際に散見されるのが以下の実装である.
while(!finish) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
だが, これだとReachabilityのようなイベントがごく稀 (環境によるが...) の場合は, finish = YES
したところでRunLoopが中々終了しない.
これは, distantFuture
でinput source
が発火するまで待つように指示している為である.
では, 以下の要件を満たす為にはどうすれば良いのか. というのがこの記事の趣旨である.
- 短期間のポーリングによる負荷は避けたい為,
distantFuture
指定は維持したい - 終了したいタイミングで, 出来る限り早く, 確実に終了したい.
解決策
/******** 起動時 *********/
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block NSRunLoop* runLoop = nil;
__block BOOL finish = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
runLoop = [NSRunLoop currentRunLoop];
dispatch_semaphore_signal(sem);
while(!finish) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // runLoopに値が代入されるのを待つ為
/******* 終了時 ********/
finish = YES;
NSTimer* schedulerSignal = [NSTimer timerWithTimeInterval:0 repeats:NO block:^{ /* NOP */ }];
[runLoop addTimer:schedulerSignal forMode:NSDefaultRunLoopMode];
distantFuture
はinput source
が発火する待つのであれば, input source
が発火するようにすれば良いという話である.
また, finish
にsemaphore
を使う実装もあるが, BOOLは1stepでアクセスできるので, これについてはどちらでも変わらないと思われる.
(semaphore
ではinput source
代わりにはならないので, フラグ以上の意味はない)
捕捉: NSTiemr # timerWithTimeInterval:repeats:block:はiOS10.0+なので, それ以前の場合は適当に読み替えてください. Swiftの人もよしなに読み替えてください.