woshidan's blog

そんなことよりコードにダイブ。

iOSでアプリがバックグラウンドへ遷移してもタスクが終了するまではアプリのプロセスをkillさせないようにする

AndroidではAndroid Oからバックグラウンド処理の実行制限が厳しくなることが話題ですが、iOSでは以前から基本的にはバックグラウンド処理はアプリがバックグラウンドに回った時点で停止させられます。

実際にはiOS側の判断で止めるので、タスクがすぐにkillされる状況は少なくともiOS10の段階ではなかなか開発段階の状況では再現できなかったりします*1。しかし、少なくとも実装時はバックグラウンドに回ったタスクはすぐkillされても困らない前提で書く必要があります。

そして、音楽アプリでの再生処理や地図アプリ用の位置情報の取得、ファイルのダウンロードなどバックグラウンドになってもしばらくの間動き続けることが保証されていてほしいタスクはよくあります。

この場合の対処としてできることは3種類紹介されており、それぞれ概要としては、

  1. Foregroundで短いタスクを開始する場合は、アプリがバックグラウンドへ遷移するときにそのタスクが終了するまでの時間を要求することができる
  2. Foregroundでダウンロード処理を開始する場合は、ダウンロード中にアプリが停止ないし終了してもよいようにそのダウンロード処理の管理をシステム側へ手渡すことができる
  3. 特定の種類のタスク(音楽再生など)を支援するためにバックグラウンドで実行する必要がある場合は、その支援タスクに1つ以上のバックグラウンドでの実行モードを宣言することができる

となっています。

今回は、1. のケースについての実装についてメモします。

実装概要

  1. beginBackgroundTaskWithExpirationHandler: を使ってiOSのSystemへバックグラウンドのタスクを実行するための追加の実行時間をリクエストする。引数のブロックには追加の実行時間が終わっても処理が終了していなかった場合の後処理の内容を記述します。最低限書く必要がある内容は、後の項目の 3. に記載するiOSのSystemに対するタスクの終了通知です。
    1. の際に、何のタスク用、あるいはコードのどこでリクエストしたのかの識別するための値として UIBackgroundIdentifierの値を受け取ります。
  2. GCDのキューなどにバックグラウンドで実行したいタスクを渡して実行します。タスクの最後で、 2. の値を引数に endBackgroundTask: を実行して、iOSのSystemに当該タスクは終了したため、そのために実行時間を取らなくていいことを伝えます。

サンプルコード

// https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html より
UIBackgroundIdentifier *bgTask;

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // リクエストした実行時間が切れてしまい、途中終了する場合に呼ばれる後処理を書く
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // バックグラウンドに入っても継続して実行したい処理を書く
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

endBackgroundTask: はUIスレッドからもバックグラウンドスレッドからも呼び出されていますが、ドキュメントによれば問題ありません。

最初、beginBackgroundTaskWithName:expirationHandler:ないしbeginBackgroundTaskWithExpirationHandler:は、公式やQiitaなどにあるサンプルコードの表面だけ見ると applicationDidEnterBackground:メソッドなど、Backgroundに遷移する前のタイミングで一回呼び出せばいいようなバックグラウンドに入る前に一度呼び出すもののように見えました。

しかし、beginBackgroundTaskWithExpirationHandler:によれば、Backgroundに遷移したら中断されたらユーザー体験に影響があるようなタスクのたびにこのメソッドを呼ぶべきだしアプリの実行中のどこで呼ぶこともできる、と書いてあるので、必要なタイミングで begin~end~ が対応するように呼んだほうがわかりやすいかもしれません。

なお、実行中のBackground処理の残り時間が必要な場合は backgroundTimeRemaining プロパティにて確認可能です。

現場からは以上です。

参考

*1:特に、実行時に他のアプリが動いていないシミュレータなどでは(苦笑)。