西海岸より

つらつらざつざつと

重い処理途中の再起動で落ちない方法

iOSでは、ホームボタンを押して5秒以内に(メインスレッドで)処理中のメソッドが終了しない場合は、強制的にアプリが終了させられる制約があり、この場合アプリはバックグラウンドからも削除されてしまう。
とはいえ時間のかかる長い処理をしたいことは多々あるので、回避方法を紹介。

処理時間の長いメソッド中に、メインスレッドの制御を戻す処理を入れる

そもそも、ホームボタンを押した際に5秒以上処理が続けば強制終了される、というのは、ホームボタンを押してサスペンドされる際にコールされるAppDelgateのapplicationDidEnterBackgroundメソッドが、呼ぶことができないため。
(applicationDidEnterBackgroundメソッドはメインスレッドで実行されるが、メインスレッド処理中であり占有されているため)

なので、長いメソッドの中に、メインスレッドの制御を一時的に戻す処理を適度に入れてやればよい。

具体的には以下の通り。

- (void)doLongTask {
   // 何らかの重い処理 (a)

  // ここでメインスレッドの制御を一時的に戻す
  // 0.5秒waitをかける。
  //   (この間にapplicationDidEnterBackgroundなどのメインスレッドのメソッドのコールが可能)
  [self waitSeconds:0.5f];  

   // 何らかの重い処理 (b)
}

- (void)waitSeconds:(NSTimeInterval)seconds {
  NSRunLoop *theRL = [NSRunLoop currentRunLoop];
  [theRL runMode:NSDefaultRunLoopMode 
             beforeDate:[NSDate dateWithTimeIntervalSinceNow:seconds];
}

waitSeoncdsメソッドの実態は、メインスレッドの制御を一時的に戻す処理であり、このwait中にapplicationDidEnterBackgroundの処理が実行できるようになる。
このようなwait処理は汎用的に使えるようにしておくと便利なので、
自分の場合はNSObjectのカテゴリで実装している。

  • NSObject+WaitAddition.h
@interface NSObject (WaitAddition)

- (void)waitSeconds:(NSTimeInterval)seconds;

@end
  • NSObject+WaitAddition.m
@implementation NSObject (WaitAddition)

- (void)waitSeconds:(NSTimeInterval)seconds {
  NSRunLoop *theRL = [NSRunLoop currentRunLoop];
  [theRL runMode:NSDefaultRunLoopMode 
             beforeDate:[NSDate dateWithTimeIntervalSinceNow:seconds];
}

@end

目安としては、処理時間の長いメソッドに対して、大体3秒の処理単位ごとに上記メソッドを挿入するとよい。

これまでメインスレッドに制御を戻すと言えば、非同期処理しかないと思っていたが、このようにメソッドの中にwait処理を入れるだけでよいので可視性を損なわず書きやすい。