Android

Oreoで「java.lang.IllegalStateException: Not allowed to start service Intent { ~ }: app is in background uid ~」が出た

android_oreo

先日、開発していたアプリのtargetSdkVersionを26に移行しました。
すると、バックグラウンドでサービスを立ち上げる際に、アプリがクラッシュするようになったのです。
エラーには「Not allowed to start service ~」と書かれていました。
その際に使用していた端末はPixel(バージョン8.1.0)です。

何事かと思い、色々と確認したところ、Android8(Oreo)からはアプリがバックグラウンドの状態でのサービスに制限が加わったそうです。。
参考:バックグラウンド実行制限

今回は、その際に調べたことと、対応方法についてご紹介します。

Android8からはアプリがバックグラウンドの状態だとサービスに制限がかかる

最近のGoogleは、Androidのバッテリー消費を少なくするために、いろいろな節電方法を導入しています。
今回は、バックグラウンドにあるアプリで実行される動作を2つ制限しにきました。

アプリの動作は次の 2 つの方法で制限されます。
バックグラウンドサービスの制限事項: アプリがアイドル状態にある場合、バックグラウンドサービスの使用を制限します。 これは、ユーザーが認識しやすいフォアグラウンドサービスには適用されません。
ブロードキャストの制限事項: 限定的な例外を除き、アプリはマニフェストを使用して暗黙的なブロードキャストを登録できません。 ただし、アプリは実行時にこれらのブロードキャストを登録でき、アプリを明確に対象とする明示的なブロードキャストについては、マニフェストを使って登録できます。

バックグラウンド実行制限 概要

これにより、ユーザーエクスペリエンスを向上させられるそうです。

確かに、端末にかかる負荷が減ると動作が早くなったり、バッテリーが長く持つようになったりするので、1ユーザーとしては嬉しいことだと思います。

ただ、開発者としてはサービスの実行方法について検討しなおしたり、修正を加えたりしなければならないので、かなり大変です。

このバックグラウンド実行制限により、サービスはアプリがバックグラウンドのときに、以下のような制限を受けるようになります。

システムは、バックグラウンドアプリによるバッグラウンドサービスの作成を許可しません。

バックグラウンド実行制限 バックグラウンドサービスの制限事項

つまり、アプリをバックグラウンドにした状態でstartService()を呼んでしまうと、アプリがクラッシュしてしまうわけです。

ただし、アプリが以下のような作業をしている間は、この制限を受けないホワイトリストに入れられるそうです。

・高い優先度の Firebase Cloud Messaging(FCM)メッセージの処理。
・SMS/MMS メッセージなどのブロードキャストの受信。
・通知からの PendingIntent の実行。

バックグラウンド実行制限 バックグラウンドサービスの制限事項

これらの処理中は制限から除外されるということですね。

このあたりの細かい条件については、以下のQiitaの記事がとても参考になりますので、一度目を通しておくと良いですよ。
Android Oからのバックグラウンド・サービスの制限事項を実演する。

Android8の端末でバックグラウンドでサービスを使用する方法

JobSchedulerに置き換える

Googleは、バックグラウンドサービスをJobSchedulerに置き換えることを推奨しているようです。

多くの場合、アプリではバックグラウンドサービスをJobSchedulerジョブに置き換えることができます。

バックグラウンド実行制限 バックグラウンドサービスの制限事項

ただ、JobSchedulerはAPI level 21から加わったものなので、それ以下のバージョンも扱っている場合は、端末のバージョンによって処理を分岐しなければなりません。

Context.startForegroundService()メソッドを使用する

もう一つの方法として、作成したサービスをフォアグラウンドに紐づける、というものがあります。

Android 8.0 では、フォアグラウンドで新しいサービスを作成する Context.startForegroundService()メソッドが新たに導入されています。
システムによってサービスが作成されると、アプリは、サービスのstartForeground()メソッドを 5 秒以内に呼び出して、その新しいサービスの通知をユーザーに表示します。

バックグラウンド実行制限 バックグラウンドサービスの制限事項

つまり、アクティビティやフラグメント側でContext.startForegroundService()メソッドを呼び、サービス側のonStartCommand()でstartForeground()メソッドを呼びだせば良いそうです。

実際に試してみたところ、アプリがバックグラウンド状態でも問題なくサービスが実行できました。
しかし、その代わりなのか、通知バーに「アプリが電池を消費しています」という通知が表示されるようになってしまいました。
この通知が表示されてしまうと、ユーザーへの印象が少し悪くなりそうで怖いですね。

どちらを選ぶかは開発者次第といったところでしょうか。

終わりに

今回は、Andriod8でアプリがバックグラウンドの状態の場合、サービスが実行できない件についてご紹介しました。
対応方法としては、
・JobSchedulerを使う
・Context.startForegroundService()メソッドを使う
のどちらかのようです。
以上です。それでは。