タスクスケジューラの進化



私たちが取り組んでいるiFunnyアプリケーションは、5年以上店頭で利用できます。 この間、モバイルチームはツール間でさまざまなアプローチと移行を行う必要があり、1年前に自作のソリューションから切り替えて、より「ファッショナブル」で広範なものの方向性を検討する時間がありました。 この記事は、研究されていること、解決策が検討されていること、そしてそれらが何になったのかについて、少し絞ったものです。

なぜこれがすべて必要なのですか?

この記事が何であるか、そしてこのトピックがAndroid開発チームにとって重要であることが判明した理由を称えて、すぐに決定しましょう。

  1. アクティブなユーザーインターフェイスのフレームワーク外でタスクを実行する必要がある場合、多くのシナリオがあります。
  2. システムは、そのようなタスクの起動に多数の制限を課します。
  3. 各ツールには長所と短所があるため、既存のソリューションから選択することは非常に困難であることが判明しました。

イベントの開発の年表


Android 0

AlarmManager、ハンドラ、サービス


最初は、サービスに基づいてバックグラウンドベースのタスクを起動するためのソリューションが実装されていました。 また、タスクをライフサイクルにリンクし、それらをキャンセルおよび復元できるメカニズムがありました。 プラットフォームはこのようなタスクに制限を課していないため、これは長い間チームに適していました。
Googleは、次の図に基づいてこれを行うことを推奨しました。



2018年末には、これを理解する意味はありません。災害の規模を評価するだけで十分です。
実際、バックグラウンドでどれだけの作業が行われているのか、誰も気にしませんでした。 アプリケーションは、必要なときに必要なことを行いました。

長所
どこでも利用可能。
すべてにアクセスできます。

短所
システムはあらゆる方法で作業を制限します。
条件による起動なし;
APIは最小限であり、多くのコードを記述する必要があります。

Android 5.ロリポップ

Jobcheduler


2015年に近い5(!)年後、Googleはタスクが非効率的に起動されることに気付きました。 ユーザーは、テーブルやポケットに横たわっているだけで、携帯電話の残量が少なくなっていることを定期的に訴え始めました。

Android 5のリリースで、JobSchedulerのようなツールが登場しました。 これは、さまざまな作業をバックグラウンドで実行できるメカニズムです。これらのタスクは、これらのタスクの集中起動システムとこの起動の条件を設定する機能により、最適化および簡素化されました。

コードでは、これは非常に単純に見えます。開始イベントと終了イベントが発生するサービスがアナウンスされます。
ニュアンスから:非同期で作業を実行する場合は、onStartJobからストリームを開始する必要があります。 主なことは、作業の最後にjobFinishedメソッドを呼び出すことを忘れないことです。そうしないと、システムはWakeLockを解放せず、タスクは完了したと見なされず、失われます。

public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 

アプリケーションのどこからでも、この作業を開始できます。 タスクはプロセスで実行されますが、IPCレベルで開始されます。 実行を制御し、これに必要なタイミングでのみアプリケーションを起動する集中化されたメカニズムがあります。 また、さまざまなトリガー条件を設定し、バンドルを介してデータを転送できます。

 JobInfo task = new JobInfo.Builder(JOB_ID, serviceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build(); JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); scheduler.schedule(task); 

一般に、何もないのと比較して、これはすでに何かでした。 ただし、このメカニズムはAPI 21でのみ使用できます。Android5.0のリリース時に、すべての古いデバイスのサポートを停止するのは奇妙です(3年が経過し、4をサポートしています)。

長所
APIはシンプルです。
実行する条件。

短所
API 21以降で利用可能
実際、API 23のみで。
間違いを犯しやすい。

Android 5.ロリポップ

GCMネットワークマネージャー


JobSchedulerの類似版-GCM Network Managerも発表されました。 これは、同様の機能を提供するライブラリですが、API 9で既に機能していました。確かに、見返りにGoogle Playサービスが必要でした。 どうやら、JobSchedulerが機能するために必要な機能は、AndroidバージョンだけでなくGPSレベルでも提供されるようになりました。 フレームワークの開発者は非常に迅速に考えを変え、将来をGPSに接続しないことに決めたことに注意する必要があります。 彼らに感謝します。

すべてがまったく同じに見えます。 同じサービス:

 public class GcmNetworkManagerService extends GcmTaskService { @Override public int onRunTask(TaskParams taskParams) { doWork(taskParams); return 0; } } 

同じタスクの起動:

 OneoffTask task = new OneoffTask.Builder() .setService(GcmNetworkManagerService.class) .setTag(TAG) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) .build(); GcmNetworkManager mGcmNetworkManager = GcmNetworkManager.getInstance(this); mGcmNetworkManager.schedule(task); 

アーキテクチャのこの類似性は、継承された機能と、ツール間の単純な移行を取得したいという要望によって決まりました。

長所
JobSchedulerに似たAPI。
API 9以降で使用可能です。

短所
Google Play開発者サービスが必要です
間違いを犯しやすい。

Android 5.ロリポップ

WakefulBroadcastReceiver


次に、JobSchedulerで使用され、開発者が直接利用できる基本的なメカニズムの1つについていくつか説明します。 これはWakeLockとそのベースのWakefulBroadcastReceiverです。

WakeLockを使用すると、システムがサスペンド状態のままになるのを防ぐことができます。つまり、デバイスをアクティブな状態に保つことができます。 これは、重要な作業を行う場合に必要です。
WakeLockを作成するとき、その設定を指定できます。CPU、画面、またはキーボードを保持します。

 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE) PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name") wl.acquire(timeout); 

このメカニズムに基づいて、WakefulBroadcastReceiverが機能します。 サービスを開始し、WakeLockを保持します。

 public class SimpleWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SimpleWakefulService.class); startWakefulService(context, service); } } 

サービスが必要な作業を完了した後、同様の方法でリリースします。

4つのバージョンにより、このBroadcastReceiverは廃止され、developer.android.comで次の代替手段が説明されます。


Android 6.マシュマロ

DozeMode:外出先でのスリープ


その後、Googleはデバイスで実行されるアプリケーションにさまざまな最適化を適用し始めました。 しかし、ユーザーにとっての最適化とは、開発者にとっての制限です。

最初のステップはDozeModeで、一定時間アイドル状態になるとデバイスをスリープモードにします。 最初のバージョンでは、これは1時間続きましたが、その後のバージョンでは、スリープ時間が30分に短縮されました。 定期的に、電話機は起動し、すべての保留中のタスクを実行し、再びスリープ状態になります。 DozeModeウィンドウは指数関数的に拡大します。 モード間のすべての遷移は、adbを介して追跡できます。

DozeModeが発生すると、アプリケーションに次の制限が課されます。


DozeModeの制限に該当しないようにアプリケーションをホワイトリストに追加することもできますが、少なくともSamsungはこのリストを完全に無視しました。

Android 6.マシュマロ

AppStandby:非アクティブなアプリケーション


システムは、非アクティブなアプリケーションを識別し、DozeModeと同じ制限をすべてそれらに課します。
次の場合、アプリケーションは分離に送信されます。


Android 7.ヌガー

バックグラウンドの最適化。 スベルト


Svelteは、Googleがアプリケーションとシステム自体によるRAM消費を最適化しようとしているプロジェクトです。
Android 7では、このプロジェクトのフレームワーク内で、暗黙のブロードキャストは非常に効果的ではないことが決定されました。なぜなら、それらは膨大な数のアプリケーションでリッスンされ、これらのイベントが発生するとシステムが大量のリソースを消費するためです。 したがって、次のタイプのイベントは、マニフェストでの宣言が禁止されていました。


Android 7.ヌガー

FirebaseJobDispatcher


同時に、タスク起動フレームワークの新しいバージョンであるFirebaseJobDispatcherが公開されました。 実際、それは完成したGCM NetworkManagerであり、少し整理され、少し柔軟になりました。

視覚的には、すべてがまったく同じに見えました。 同じサービス:

 public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 

彼との唯一の違いは、ドライバーをインストールできることです。 ドライバーは、タスクの起動戦略を担当したクラスです。

タスクの起動自体は時間の経過とともに変化していません。

 FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); Job task = dispatcher.newJobBuilder() .setService(FirebaseJobDispatcherService.class) .setTag(TAG) .setConstraints(Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_IDLE) .build(); dispatcher.mustSchedule(task); 

長所
JobSchedulerに似たAPI。
API 9以降で使用可能です。

短所
Google Play開発者サービスが必要です
間違いを犯しやすい。

GPSを取り除くためにドライバーをインストールすることは勇気づけられました。 検索もしましたが、最終的には次のことがわかりました。





Googleはこれを知っていますが、これらのタスクは数年間開いています。

Android 7.ヌガー

EvernoteによるAndroidジョブ


その結果、コミュニティはそれに耐えられず、Evernoteのライブラリの形で自作のソリューションが登場しました。 それだけではありませんでしたが、Evernoteのソリューションであり、それ自体を確立し、「人々に夢中になった」ことができました。

アーキテクチャ面では、このライブラリは前のライブラリよりも便利でした。
タスクの作成を担当するエンティティが登場しました。 JobSchedulerの場合、それらはリフレクションによって作成されました。

 class SendLogsJobCreator : JobCreator { override fun create(tag: String): Job? { when (tag) { SendLogsJob.TAG -> return SendLogsJob() } return null } } 

タスク自体である別のクラスがあります。 JobSchedulerでは、これはすべてonStartJob内のスイッチにダンプされました。

 class SendLogsJob : Job() { override fun onRunJob(params: Params): Result { return doWork(params) } } 

タスクの起動は同じですが、Evernoteは継承されたイベントに加えて、毎日のタスク、独自のタスク、ウィンドウ内での起動など、独自のタスクを追加しました。

 new JobRequest.Builder(JOB_ID) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .build() .scheduleAsync(); 

長所
便利なAPI。
すべてのバージョンでサポートされています。
Google Playサービスは必要ありません。

短所
サードパーティのソリューション。

彼らはライブラリを積極的にサポートしました。 重大な問題がかなりありましたが、すべてのバージョンとすべてのデバイスで機能しました。 その結果、昨年Googleのライブラリがサポートできないデバイスの大きなレイヤーをカットしたため、Androidチームは昨年Evernoteのソリューションを選択しました。
内部では、極端な場合にAlarmManagerを使用してGoogleのソリューションに取り組みました。

Android 8.オレオ

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


制限に戻りましょう。 新しいAndroidの登場により、新しい最適化が行われました。 Googleのスタッフは別の問題を発見しました。 今回はすべてサービスとブロードキャストについてでした(はい、新しいものはありません)。


まず、バックグラウンドからサービスを開始することは禁止されていました。 「法の枠組み」では、フォアグラウンドサービスのみでした。 現在、サービスは廃止されていると言えます。
2番目の制限は同じブロードキャストです。 今回は、マニフェストへのすべての暗黙的なブロードキャストの登録が禁止されました。 暗黙的ブロードキャストは、私たちのアプリケーションだけでなく、ブロードキャストです。 たとえば、アクションACTION_PACKAGE_REPLACEDがあり、ACTION_MY_PACKAGE_REPLACEDがあります。 したがって、最初のものは暗黙的です。

ただし、ブロードキャストはContext.registerBroadcastを介して登録できます。

Android 9.パイ

ワークマネージャー


この最適化はまだ停止しています。 おそらく、デバイスはエネルギー消費の観点から迅速かつ慎重に動作し始めたのでしょう。 おそらく、ユーザーはそれについてあまり苦情を言わないでしょう。
Android 9では、フレームワークの開発者がタスクを起動するツールに徹底的にアプローチしました。 すべての差し迫った問題を解決するために、WorkManagerのバックグラウンドタスクを起動するためのライブラリがGoogle I / Oで起動されました。

Googleは最近、Androidアプリケーションのアーキテクチャに関するビジョンを形成しようとしており、開発者にこれに必要なツールを提供しています。 そのため、LiveData、ViewModel、およびRoomを含むアーキテクチャコンポーネントがありました。 WorkManagerは、アプローチとパラダイムを合理的に補完するように見えます。

WorkManagerが内部にどのように配置されているかを説明する場合、技術的なブレークスルーはありません。 本質的に、これは既存のソリューションのラッパーです:JobScheduler、FirebaseJobDispatcher、AlarmManager。

createBestAvailableBackgroundScheduler
 static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager) { if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) { return new SystemJobScheduler(context, workManager); } try { return tryCreateFirebaseJobScheduler(context); } catch (Exception e) { return new SystemAlarmScheduler(context); } } 


選択コードは非常に簡単です。 ただし、JobSchedulerはAPI 21以降で使用できますが、最初のバージョンはかなり不安定だったため、API 23でのみ使用することに注意してください。

バージョンが23未満の場合、リフレクションを通じてFirebaseJobDispatcherを見つけようとします。そうでない場合は、AlarmManagerを使用します。

ラッパーが非常に柔軟になったことは注目に値します。 今回、開発者はすべてを個別のエンティティに分割し、アーキテクチャ的には便利に見えます:





起動条件はJobSchedulerから継承されました。
URIを変更するトリガーはAPI 23でのみ出現したことに注意してください。さらに、メソッドのフラグを使用して、特定のURIだけでなく、すべてのネストされたURIの変更にサブスクライブできます。

私たちについて話すと、アルファ段階でWorkManagerに切り替えることが決定されました。
これにはいくつかの理由があります。 Evernoteには、ライブラリの開発者がWorkManagerが統合されたバージョンへの移行で修正することを約束するいくつかの重大なバグがあります。 また、Googleの決定がEvernoteの利点を無効にすることに彼らも同意しています。 さらに、このソリューションは、アーキテクチャコンポーネントを使用しているため、アーキテクチャに適しています。

次に、このアプローチをどのように使用するかを簡単な例で示したいと思います。 同時に、WorkManagerを持っているかJobSchedulerを持っているかはそれほど重要ではありません。



非常に単純なケースの例を見てみましょう。「再公開」などをクリックします。

現在、すべてのアプリケーションはネットワークへのブロック要求から逃れようとしています。これにより、ユーザーは緊張して待機しますが、この時点でアプリケーション内で購入したり、広告を見ることができます。

このような場合、ローカルデータが最初に変更されます-ユーザーはすぐにアクションの結果を確認します。 その後、バックグラウンドでサーバーへの要求があり、サーバーが失敗した場合、データは初期状態にリセットされます。

次に、それがどのように見えるかの例を示します。

JobRunnerには、タスクを起動するためのロジックが含まれています。 彼のメソッドは、タスクの構成を記述し、パラメーターを渡します。

JobRunner.java
 fun likePost(content: IFunnyContent) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val input = Data.Builder() .putString(LikeContentJob.ID, content.id) .build() val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java) .setInputData(input) .setConstraints(constraints) .build() WorkManager.getInstance().enqueue(request) } 


WorkManager内のタスク自体は次のとおりです。パラメーターからIDを取得し、サーバー上のメソッドを呼び出してこのコンテンツを評価します。

次のロジックを含む基本クラスがあります。

 abstract class BaseJob : Worker() { final override fun doWork(): Result { val workerInjector = WorkerInjectorProvider.injector() workerInjector.inject(this) return performJob(inputData) } abstract fun performJob(params: Data): Result } 

まず、Workerの明示的な知識から少し離れることができます。 また、WorkerInjectorによる依存関係注入ロジックも含まれています。

WorkerInjectorImpl.java
 @Singleton public class WorkerInjectorImpl implements WorkerInjector { @Inject public WorkerInjectorImpl() {} @Ovierride public void inject(Worker job) { if (worker instanceof AppCrashedEventSendJob) { Injector.getAppComponent().inject((AppCrashedEventSendJob) job); } else if (worker instanceof CheckNativeCrashesJob) { Injector.getAppComponent().inject((CheckNativeCrashesJob) job); } } } 


Daggerへの呼び出しを単にプロキシしますが、テストには役立ちます。インジェクターの実装を置き換え、タスクに必要な環境を実装します。

 fun void testRegisterPushProvider() { WorkManagerTestInitHelper.initializeTestWorkManager(context) val testDriver = WorkManagerTestInitHelper.getTestDriver() WorkerInjectorProvider.setInjector(TestInjector()) // mock dependencies val id = jobRunner.runPushRegisterJob() testDriver.setAllConstraintsMet(id) Assert.assertTrue(…) } 

 class LikePostInteractor @Inject constructor( val iFunnyContentDao: IFunnyContentDao, val jobRunner: JobRunner) : Interactor { fun execute() { iFunnyContentDao.like(getContent().id) jobRunner.likePost(getContent()) } } 

インタラクターは、スクリプトの通過を開始するためにViewControllerがプルするエンティティです(この場合、このように)。 コンテンツをローカルで「アップロード済み」としてマークし、実行のためにタスクを送信します。 タスクが失敗すると、同様のものが削除されます。

 class IFunnyContentViewModel(val iFunnyContentDao: IFunnyContentDao) : ViewModel() { val likeState = MediatorLiveData<Boolean>() var iFunnyContentId = MutableLiveData<String>() private var iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId(); init { likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) } } } 

GoogleのアーキテクチャコンポーネントであるViewModelとLiveDataを使用します。 これがViewModelの外観です。 ここで、DAOのオブジェクトの更新をlikeのステータスに関連付けます。

IFunnyContentViewController.java
 class IFunnyContentViewController @Inject constructor( private val likePostInteractor: LikePostInteractor, val viewModel: IFunnyContentViewModel) : ViewController { override fun attach(view: View) { viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) }) } fun onLikePost() { likePostInteractor.setContent(getContent()) likePostInteractor.execute() } } 


ViewControllerは、一方では同類のステータスの変更にサブスクライブし、他方では必要なスクリプトの通過を開始します。

そして、それは私たちが必要とする実質的にすべてのコードです。 DAOの実装と同様に、View自体の動作を追加することは残っています。 ルームを使用する場合は、オブジェクトにフィールドを登録するだけです。 とてもシンプルで効果的です。

まとめると


JobScheduler、GCM Network Manager、FirebaseJobDispatcher:


EvernoteによるAndroidジョブ:


WorkManager:

Source: https://habr.com/ru/post/J434680/


All Articles