TL;DR
- HandlerとLooperを理解すれば怖いものはない
Androidの非同期処理
非同期処理の定義を漁ってみると、ざっくり下記のようにまとめられていました。
コンピュータプログラムの実行制御の手法の一つで、あるプログラムが別のプログラムを呼び出したり、プログラム内で関数やサブルーチンなどを呼び出す際に、呼び出し側が呼び出し先の完了や応答を待たずに次の処理に進む方式。
Androidアプリを作っていると時たま非同期処理を行いたくなります。 その際、ユーザーに「処理中であること」を下記のように表示することが推奨されています。
なお 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.
ユーザー操作は可能な限り妨げられるべきではない、というのが今のAndroidアプリの風潮です。 もちろん、それゆえに「あえてユーザーに操作をさせない」という選択肢も出てきます。例えばユーザーアカウント作成中など、(事前に確認の上で)中断やキャンセルができないケースです。
「ユーザー操作が妨げられない」ということは Activity
や Fragment
の破棄を想定しなければなりません。
そうなるとメモリリークを避けるために、 Activity
や Fragment
の状態に合わせて非同期処理をキャンセルする場面が出てきます。
その上で、Androidは画面描画を行うことができるスレッドが限られています。結果として非同期処理を実行するスレッドと、画面描画を行うスレッドを繋ぐ必要があります。
この辺りが、Androidの非同期処理を考える上で話をややこしくしている箇所かなと思っています。
Handler(Looper) + Thread or Executer or ...
作成したワーカースレッドの結果をUIスレッド(MainLooper)に送る方法が一番素朴な方法であり、すべての基本となります。
Androidにおけるメインスレッド、UIスレッドを抑えるためには Handler
と Looper
を抑える必要があります。
Handler
と Looper
については、「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しているだけですね。
Acitivy上で非同期処理を呼び出す設計は、あまり良いものとされない(テスタビリティが低い、ActivityがFatになる、など)ので、使うシーンはまずないと思っていいはずです。
AsyncTask/AsyncTaskLoader/AsyncTaskLoader(at Support Lib)
AsyncTaskはAPI level 3から、AsyncTaskLoaderはAPI level 11から存在するクラスです。
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内に通信などのロジックを記載することになってしまいます。
結果として、小規模なアプリにおいては利用シーンはある(?)のですが、大規模アプリではまず採用されなくなりました。 個人的には、後述の方法に比べるとキャンセル処理が煩雑になるため、可能な限り使いたくないなぁと思っています。
RxJava + RxAndroid
2015年ごろからAndroid開発の非同期処理といえばRxJava、という風潮がありました。
RxJavaでは Schedulers
として非同期処理を行うスレッドプールが用意されています。
ただ、当然のことなのですが Schedulers
はAndroid開発に最適化はされていません。
そこで、RxJavaをAndroidで利用しやすくする(=UIスレッドを取得しやすくする)ライブラリが登場しました、RxAndroidです。
実装を確認してみると、こちらも getMainLooper
していることがわかります。
この AndroidSchedulers.mainThread()
により observeOn
にAndroidのUIスレッドを指定することが簡単にできるようになりました。
また、RxJavaの処理は Disposable
インターフェイスにより処理の中断を簡単に行うことができるようになりました。
この Disposable
の実態をAndroidの各ライフサイクルに合わせて dispose()
することにより、ライフサイクルに合わせて非同期処理を漏れなくキャンセルすることが可能になっています。
RxJavaは非常に利便性が高いため、RxBindingのようにUIパーツのイベントハンドリングに利用されたり、RetrofitのAdapterとして通信処理のハンドリングに利用されたりしています。
Kotlin Coroutines
Kotlin 1.3から正式リリースとなったのがKotlin Coroutinesです。
https://kotlinlang.org/docs/reference/coroutines-overview.htmlkotlinlang.org
個人的には、Kotlin Coroutinesの利用が進んでいるのは下記2点があると思っています。
- RxJavaのSingle/Completeしか利用されないケースが多く、かつそれが通信処理の待ち合わせなどの本来Rxが対応するべき処理でないもので濫用されていた
- RxJavaの学習コストが高く、またRxのストリーム処理を起因とするバグの調査が非常に難しいものとなった
Kotlin Coroutines自体がRxJavaに比べて軽量である(メモリ使用量も、ライブラリサイズも)点が決め手になっているプロジェクトも少なくないかなと思います。
詳細は下記のブログをご参照ください。この記事のおかげでどれだけのKotlin Coroutines初心者が救われているのだろうか……。
Kotlinの Dispatchers.Main
も getMainLooper
することで、Androidの世界のUIスレッドを取得しています。
Dispatchers.Main
から下記のルートで辿ってみることができるので、確認してみてください。
Dispatchers.Main
の中身はMainDispatcherLoader.dispatcher
MainDispatcherLoader.dispatcher
はフィールドの初期化時にMainDispatcherFactory
のリストに対してtryCreateDispatcher
している- kotlinx.coroutines/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt at 1.3.0 · Kotlin/kotlinx.coroutines · GitHub
MainDispatcherFactory
はinterfaceのため、各メソッドはimplementationした実体にお任せ
- Android向けの実装となるため
kotlinx.coroutines.android.HandlerDispatcher
に定義されているAndroidDispatcherFactory
が該当- kotlinx.coroutines/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt at 1.3.0 · Kotlin/kotlinx.coroutines · GitHub
HandlerDispatcher
はLooper.getMainLooper().asHandler(async = true)
して作成したHandler
に"Main"
と名付けてHandlerContext
化するHandlerContext
はMainCoroutineDispatcher
を継承し、MainCoroutineDispatcher
がCoroutineDispatcher
を継承HandlerDispatcher
がpackage kotlinx.coroutines.android
、それ以外はpackage kotlinx.coroutines
なので、Android向けのMain or UI用CoroutineDispatcher
がHandlerDispatcher
になります
Kotlin Coroutinesも Job
をキャンセルすることで、Androidの Activity
や Fragment
のUIライフサイクルに合わせて非同期処理をキャンセルすることができます。
Kotlin CoroutinesとAndroid Architecture Components
KotlinがAndroidの正式なサポート言語となったのは2017年のGoogle IOでしたが、同時にAndroid Architecture Component(AAC)も発表されました。
このため、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を検討
大変ざっくりとしたまとめでしたが、ご一読いただきありがとうございました。