Continuity is The Father of Success

Androidアプリとかゲームとか。毎日続けてるものについて。

Androidの非同期処理について自分なりの整理

TL;DR

  • HandlerとLooperを理解すれば怖いものはない

Androidの非同期処理

非同期処理の定義を漁ってみると、ざっくり下記のようにまとめられていました。

コンピュータプログラムの実行制御の手法の一つで、あるプログラムが別のプログラムを呼び出したり、プログラム内で関数やサブルーチンなどを呼び出す際に、呼び出し側が呼び出し先の完了や応答を待たずに次の処理に進む方式。

e-words.jp

Androidアプリを作っていると時たま非同期処理を行いたくなります。 その際、ユーザーに「処理中であること」を下記のように表示することが推奨されています。

material.io

なお ProgressDialog は下記の理由でAPI 26よりDeprecatedになりました。

ProgressDialog is a modal dialog, which prevents the user from interacting with the app. Instead of using this class, you should use a progress indicator like ProgressBar, which can be embedded in your app's UI. Alternatively, you can use a notification to inform the user of the task's progress.

developer.android.com

ユーザー操作は可能な限り妨げられるべきではない、というのが今のAndroidアプリの風潮です。 もちろん、それゆえに「あえてユーザーに操作をさせない」という選択肢も出てきます。例えばユーザーアカウント作成中など、(事前に確認の上で)中断やキャンセルができないケースです。

「ユーザー操作が妨げられない」ということは AcitivyFragment の破棄を想定しなければなりません。 そうなるとメモリリークを避けるために、 ActivityFragment の状態に合わせて非同期処理をキャンセルする場面が出てきます。 その上で、Androidは画面描画を行うことができるスレッドが限られています。結果として非同期処理を実行するスレッドと、画面描画を行うスレッドを繋ぐ必要があります。

この辺りが、Androidの非同期処理を考える上で話をややこしくしている箇所かなと思っています。

Handler(Looper) + Thread or Executer or ...

作成したワーカースレッドの結果をUIスレッド(MainLooper)に送る方法が一番素朴な方法であり、すべての基本となります。

Androidにおけるメインスレッド、UIスレッドを抑えるためには HandlerLooper を抑える必要があります。

mixi-inc.github.io

HandlerLooper については、「Androidを支える技術〈Ⅰ〉──60fpsを達成するモダンなGUIシステム」の第3章をお勧めします。 本稿では、筆者の知識不足もあるため解説は試みません。

古めのサンプルコードやAndroidや標準ライブラリのソースコードを読んでいると、想像以上に getMainLooper しているコードに出会います。 ただ、2019年現在ではAndroidアプリ開発現場で Looper を直接触ることは少ないでしょう。大抵の場合、非同期処理を扱いやすくするメソッドやライブラリを利用します。 (実際は Looper の取得をライブラリなどに任せているだけの場合も多いのですが。。。)

Activity.runOnUiThread(Runnable), View.post(Runnable), View.postDelayed(Runnable, long)

ThreadをActivityやView上で立て、その処理結果を反映したい場合に使うことのできるメソッドです。 実態は動作しているスレッドを確認して、ワーカースレッドならHandlerにpostしているだけですね。

androidxref.com

Acitivy上で非同期処理を呼び出す設計は、あまり良いものとされない(テスタビリティが低い、ActivityがFatになる、など)ので、使うシーンはまずないと思っていいはずです。

AsyncTask/AsyncTaskLoader/AsyncTaskLoader(at Support Lib)

AsyncTaskはAPI level 3から、AsyncTaskLoaderはAPI level 11から存在するクラスです。

developer.android.com

developer.android.com

AsyncTaskは下記のように説明されています。

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

AsyncTaskLoaderは、AsyncTaskを経由してhandlerに行き着きます。

Abstract Loader that provides an AsyncTask to do the work. See Loader and LoaderManager for more details.

AsyncTaskはぱっと見わかりやすい処理になりますが、非同期処理のロジックとUIの表示ロジックを切り離そうとすると、(特にJavaでは)コールバック地獄が容易に発生します。 コールバックではなく、Activityのインナークラスとして定義しようとすると、今度はActivity内に通信などのロジックを記載することになってしまいます。

結果として、小規模なアプリにおいては利用シーンはある(?)のですが、大規模アプリではまず採用されなくなりました。 個人的には、後述の方法に比べるとキャンセル処理が煩雑になるため、可能な限り使いたくないなぁと思っています。

developer.android.com

RxJava + RxAndroid

2015年ごろからAndroid開発の非同期処理といえばRxJava、という風潮がありました。

techlife.cookpad.com

RxJavaでは Schedulers として非同期処理を行うスレッドプールが用意されています。 ただ、当然のことなのですが SchedulersAndroid開発に最適化はされていません。

そこで、RxJavaをAndroidで利用しやすくする(=UIスレッドを取得しやすくする)ライブラリが登場しました、RxAndroidです。

github.com

実装を確認してみると、こちらも getMainLooper していることがわかります。

github.com

この AndroidSchedulers.mainThread() により observeOnAndroidのUIスレッドを指定することが簡単にできるようになりました。 また、RxJavaの処理は Disposable インターフェイスにより処理の中断を簡単に行うことができるようになりました。

medium.com

この Disposable の実態をAndroidの各ライフサイクルに合わせて dispose() することにより、ライフサイクルに合わせて非同期処理を漏れなくキャンセルすることが可能になっています。 RxJavaは非常に利便性が高いため、RxBindingのようにUIパーツのイベントハンドリングに利用されたり、RetrofitのAdapterとして通信処理のハンドリングに利用されたりしています。

Kotlin Coroutines

Kotlin 1.3から正式リリースとなったのがKotlin Coroutinesです。

kotlinlang.org

個人的には、Kotlin Coroutinesの利用が進んでいるのは下記2点があると思っています。

  1. RxJavaのSingle/Completeしか利用されないケースが多く、かつそれが通信処理の待ち合わせなどの本来Rxが対応するべき処理でないもので濫用されていた
  2. RxJavaの学習コストが高く、またRxのストリーム処理を起因とするバグの調査が非常に難しいものとなった

Kotlin Coroutines自体がRxJavaに比べて軽量である(メモリ使用量も、ライブラリサイズも)点が決め手になっているプロジェクトも少なくないかなと思います。

詳細は下記のブログをご参照ください。この記事のおかげでどれだけのKotlin Coroutines初心者が救われているのだろうか……。

sys1yagi.hatenablog.com

Kotlinの Dispatchers.MaingetMainLooper することで、Androidの世界のUIスレッドを取得しています。 Dispatchers.Main から下記のルートで辿ってみることができるので、確認してみてください。

Kotlin Coroutinesも Job をキャンセルすることで、AndroidActivityFragment のUIライフサイクルに合わせて非同期処理をキャンセルすることができます。

Kotlin CoroutinesとAndroid Architecture Components

KotlinがAndroidの正式なサポート言語となったのは2017年のGoogle IOでしたが、同時にAndroid Architecture Component(AAC)も発表されました。

tech.recruit-mp.co.jp

このため、Kotlin Coroutinesが発表された2018年10月にはAACが提供するMVVMアーキテクチャLiveData といった非同期処理の結果を、安全にUIスレッドへ伝える方法が整備されていました。 AACにはRxJavaをサポートする流れもあるため、必ずしもRxJavaからKotlin Coroutinesへの移行を加速させるものではないと思われます。が、現状としては徐々にKotlin Coroutinesの採用が増えている実感があります。

個人的な思い

  • Kotlin CoroutinesとAndroid Architecture Componentを組み合わせて使うのがオススメ
  • Rxは非常に詳しい or アプリの要件的に必須の時に使おう
  • AsyncTask/AsyncTaskLoaderは使わなくていいはず
  • 汎用のライブラリを使うならLooperを検討

大変ざっくりとしたまとめでしたが、ご一読いただきありがとうございました。