複数の JobService を活用する
2017年10月26日木曜日
この記事は Android DA ソフトウェア エンジニア、Isai Damier による Android Developers Blog の記事 "Working with Multiple JobServices" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
ユーザー エクスペリエンスを改善する努力が続けられる中、API レベル 26 で Android プラットフォームにバックグラウンド サービスに対する厳しい制限が導入されました。基本的に、アプリがフォアグラウンドで実行されている場合を除き、アプリのバックグラウンド サービスはシステムによって数分以内に停止されます。
このバックグラウンド サービスの制限の結果、
Android TV アプリを作っているところをイメージしてください。TV アプリではチャンネルが非常に重要です。アプリは、チャンネルに対して少なくとも 5 種類のバックグラウンド操作を行える必要があります。それは、チャンネルのパブリッシュ、チャンネルへのプログラムの追加、チャンネル ログのリモート サーバーへの送信、チャンネルのメタデータのアップデート、そしてチャンネルの削除です。Android 8.0(Oreo)より前では、この 5 つの操作をバックグラウンド サービスとして実装できました。しかし、API 26 以降では、どれを旧来のバックグラウンド
TV アプリのケースでは、前述の 5 つの操作のうち、旧来のバックグラウンド サービスとして実装できるのはチャンネルのパブリッシュのみです。状況にもよりますが、チャンネルのパブリッシュには 3 つの手順が必要となります。まず、ユーザーが処理を開始するボタンを押します。次に、アプリがバックグラウンド操作を開始してパブリッシュを行います。そして最後に、サブスクリプションを確認する UI がユーザーに提示されます。おわかりのように、チャンネルのパブリッシュにはユーザーのインタラクション、つまり目に見える Activity が必要です。そのため、ChannelPublisherService はバックグラウンド部分を扱う
一方、その他の 4 つの操作には
上記の 4 つの
この説明に書かれているのは、たとえ 4 つの異なる Java オブジェクト(-JobService)を使っていたとしても、
これは確かに大きな問題です。
おわかりのように、
1 つ目のポイント:
2 つ目のポイント: アプリ全体で 1 つの
3 つ目のポイント: アプリ内の各
次に示す
脚注: 貴重なフィードバックを寄せてくれた Christopher Tate と Trevor Johns に感謝いたします
Reviewed by Yuichi Araki - Developer Relations Team
複数の JobService を活用する
ユーザー エクスペリエンスを改善する努力が続けられる中、API レベル 26 で Android プラットフォームにバックグラウンド サービスに対する厳しい制限が導入されました。基本的に、アプリがフォアグラウンドで実行されている場合を除き、アプリのバックグラウンド サービスはシステムによって数分以内に停止されます。
このバックグラウンド サービスの制限の結果、
JobScheduler
のジョブがバックグラウンド タスクを実行する際のデファクト ソリューションになっています。サービスに詳しい方は、JobScheduler
を簡単に使うことができます。ただし、そこにはいくつかの例外的なケースもあります。ここではその 1 つをご紹介します。 Android TV アプリを作っているところをイメージしてください。TV アプリではチャンネルが非常に重要です。アプリは、チャンネルに対して少なくとも 5 種類のバックグラウンド操作を行える必要があります。それは、チャンネルのパブリッシュ、チャンネルへのプログラムの追加、チャンネル ログのリモート サーバーへの送信、チャンネルのメタデータのアップデート、そしてチャンネルの削除です。Android 8.0(Oreo)より前では、この 5 つの操作をバックグラウンド サービスとして実装できました。しかし、API 26 以降では、どれを旧来のバックグラウンド
Service
にするか、どれを JobService
にするかをよく考えた上で決めなければなりません。 TV アプリのケースでは、前述の 5 つの操作のうち、旧来のバックグラウンド サービスとして実装できるのはチャンネルのパブリッシュのみです。状況にもよりますが、チャンネルのパブリッシュには 3 つの手順が必要となります。まず、ユーザーが処理を開始するボタンを押します。次に、アプリがバックグラウンド操作を開始してパブリッシュを行います。そして最後に、サブスクリプションを確認する UI がユーザーに提示されます。おわかりのように、チャンネルのパブリッシュにはユーザーのインタラクション、つまり目に見える Activity が必要です。そのため、ChannelPublisherService はバックグラウンド部分を扱う
IntentService
にすることもできるかもしれません。ここで JobService
を使うべきでない理由は、JobService
を使うと実行時に遅延が生じる点にあります。通常、ユーザーのインタラクションにはアプリからの即時的なレスポンスが必要です。 一方、その他の 4 つの操作には
JobService
を使うべきです。この 4 つはすべて、アプリがバックグラウンド状態にあるときに実行されるからです。そのため、この 4 つの操作には、それぞれ ChannelProgramsJobService
、ChannelLoggerJobService
、ChannelMetadataJobService
、ChannelDeletionJobService
を使うべきです。 jobId の衝突を防止する
上記の 4 つの
JobService
は Channel
オブジェクトを扱うため、それぞれの channelId
を jobId
として使えると便利です。しかし、Android Framework 内での JobService
の設計手法の関係で、そうすることはできなくなっています。次に示すのは、jobId についての公式説明です。 Application-provided id for this job. Subsequent calls to cancel, or jobs created with the same jobId, will update the pre-existing job with the same id. This ID must be unique across all clients of the same uid (not just the same package). You will want to make sure this is a stable id across app updates, so probably not based on a resource ID.
この説明に書かれているのは、たとえ 4 つの異なる Java オブジェクト(-JobService)を使っていたとしても、
jobId
に同じ channelId
を使うことはできないということです。つまり、クラスレベルの名前空間を使うことはできません。 これは確かに大きな問題です。
channelId
を一連の jobId
に関連付ける安定的で拡張可能な方法が必要になります。一番やってはいけないのは、jobId
の衝突により、関係ないチャンネルの操作を互いに上書きしてしまうことです。jobId
が Integer 型ではなく String 型であれば、この問題は簡単に解決できます。その場合、ChannelProgramsJobService
に jobId= "ChannelPrograms" +
channelId
を使い、ChannelLoggerJobService
に jobId= "ChannelLogs" + channelId
を使うなどの方法が考えられます。しかし、jobId
は String 型ではなく Integer 型なので、ジョブに対して再利用可能な jobId
を生成する適切な仕組みを考える必要があります。これには、次に示す JobIdManager
のような仕組みを利用できます。 JobIdManager
クラスは、アプリのニーズに応じて微調整することができます。今回の TV アプリにおける基本的な考え方は、Channel
を扱うすべてのジョブに対して単一の channelId
を使うというものです。理解を早めるため、まずはこのサンプル JobIdManager
クラスのコードを見てみましょう。その後、説明を行います。 public class JobIdManager { public static final int JOB_TYPE_CHANNEL_PROGRAMS = 1; public static final int JOB_TYPE_CHANNEL_METADATA = 2; public static final int JOB_TYPE_CHANNEL_DELETION = 3; public static final int JOB_TYPE_CHANNEL_LOGGER = 4; public static final int JOB_TYPE_USER_PREFS = 11; public static final int JOB_TYPE_USER_BEHAVIOR = 21; @IntDef(value = { JOB_TYPE_CHANNEL_PROGRAMS, JOB_TYPE_CHANNEL_METADATA, JOB_TYPE_CHANNEL_DELETION, JOB_TYPE_CHANNEL_LOGGER, JOB_TYPE_USER_PREFS, JOB_TYPE_USER_BEHAVIOR }) @Retention(RetentionPolicy.SOURCE) public @interface JobType { } //16-1 for short. Adjust per your needs private static final int JOB_TYPE_SHIFTS = 15; public static int getJobId(@JobType int jobType, int objectId) { if ( 0 < objectId && objectId < (1<< JOB_TYPE_SHIFTS) ) { return (jobType << JOB_TYPE_SHIFTS) + objectId; } else { String err = String.format("objectId %s must be between %s and %s", objectId,0,(1<<JOB_TYPE_SHIFTS)); throw new IllegalArgumentException(err); } } }
おわかりのように、
JobIdManager
は単に接頭辞と channelId
を組み合わせて jobId
を作成しているだけです。しかし、このエレガントなシンプルさは、氷山の一角でしかありません。前提条件と以下のポイントについて考えてみましょう。 1 つ目のポイント:
channelId
と接頭辞を組み合わせても有効な Java の Integer 型の範囲内に収まるようにするために、channelId
を Short 型に強制する必要があります。もちろん、厳密に言うなら、必ずしも Short 型である必要はありません。接頭辞と channelId
を組み合わせてオーバーフローしない Integer 型を得られさえすればよいのです。しかし、健全なエンジニアリングにはマージンが欠かせません。そのため、どうしても他に選択肢がない場合以外は、Short 型を強制するとよいでしょう。実際に、リモート サーバー上に大きな ID を持つオブジェクトに対してこれを行うには、ローカル データベースやコンテンツ プロバイダでキーを定義し、そのキーを使って jobId
を生成します。 2 つ目のポイント: アプリ全体で 1 つの
JobIdManager
クラスを使う必要があります。そのクラスで、アプリのすべてのジョブの jobId
を生成します。ジョブが Channel
を扱うのか User
を扱うのか、それとも Cat
や Dog
を扱うのかは関係ありません。サンプルの JobIdManager
クラスは、この点に対処しています。すべての JOB_TYPE
に Channel
操作が関係しているわけではありません。ジョブタイプの 1 つはユーザー プリファレンスを扱っており、別の 1 つはユーザーの動作を扱っています。JobIdManager
は、ジョブタイプごとに別の接頭辞を割り当てることによって、それらすべてに対応しています。 3 つ目のポイント: アプリ内の各
-JobService
について、一意かつ final な JOB_TYPE_
接頭辞が必要です。ここでも、網羅的な 1 対 1 の関係が存在する必要があります。 JobIdManager の使用
次に示す
ChannelProgramsJobService
のコード スニペットは、プロジェクトで JobIdManager
を使用する方法の一例です。新しいジョブをスケジュールする必要がある場合は、常に jobId
を生成する必要があります。その際に、JobIdManager.getJobId(...)
を使用します。 import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.os.PersistableBundle; public class ChannelProgramsJobService extends JobService { private static final String CHANNEL_ID = "channelId"; . . . public static void schedulePeriodicJob(Context context, final int channelId, String channelName, long intervalMillis, long flexMillis) { JobInfo.Builder builder = scheduleJob(context, channelId); builder.setPeriodic(intervalMillis, flexMillis); JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); if (JobScheduler.RESULT_SUCCESS != scheduler.schedule(builder.build())) { //todo what? log to server as analytics maybe? Log.d(TAG, "could not schedule program updates for channel " + channelName); } } private static JobInfo.Builder scheduleJob(Context context,final int channelId){ ComponentName componentName = new ComponentName(context, ChannelProgramsJobService.class); final int jobId = JobIdManager .getJobId(JobIdManager.JOB_TYPE_CHANNEL_PROGRAMS, channelId); PersistableBundle bundle = new PersistableBundle(); bundle.putInt(CHANNEL_ID, channelId); JobInfo.Builder builder = new JobInfo.Builder(jobId, componentName); builder.setPersisted(true); builder.setExtras(bundle); builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); return builder; } ... }
脚注: 貴重なフィードバックを寄せてくれた Christopher Tate と Trevor Johns に感謝いたします
Reviewed by Yuichi Araki - Developer Relations Team