Quantcast
Channel: Google Developers Japan
Viewing all articles
Browse latest Browse all 2209

複数の JobService を活用する

$
0
0
この記事は Android DA ソフトウェア エンジニア、Isai Damier による Android Developers Blog の記事 "Working with Multiple JobServices" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。

複数の 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 つの操作には、それぞれ ChannelProgramsJobServiceChannelLoggerJobServiceChannelMetadataJobServiceChannelDeletionJobServiceを使うべきです。

jobId の衝突を防止する


上記の 4 つの JobServiceChannelオブジェクトを扱うため、それぞれの channelIdjobIdとして使えると便利です。しかし、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を使い、ChannelLoggerJobServicejobId= "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を扱うのか、それとも CatDogを扱うのかは関係ありません。サンプルの JobIdManagerクラスは、この点に対処しています。すべての JOB_TYPEChannel操作が関係しているわけではありません。ジョブタイプの 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

Viewing all articles
Browse latest Browse all 2209

Latest Images

Trending Articles