(趣味の)Androidアプリ開発でライフサイクルに苦しみ、とくに非同期処理周りの設計について考えさせられた。そのうち忘れそうなので、色々と考えたことをまとめておきたい。
Kotlinのコルーチンで最も特徴的なのは、スコープの存在だと思う。コルーチンはスコープと紐づけて実行し、スコープが破棄されると強制的にキャンセルされる。プロセスが死ぬとスレッドが停止するのと似ているが、プロセスが死んだあとのことを考える機会は少ない。コルーチンを使う場合はスコープを意識したい。
コルーチンの処理を実際に走らせるのはスレッドである。アプリケーションを実行しているMainスレッドと、複数のWorkerスレッドがある。どのスレッドでコルーチンを実行するかは、コルーチン開始時に指定できる。(備考: イベントループがない場合の挙動が気になったが、Dispatcher.Mainが必ず存在するとは限らない。)
AndroidのActivityやFragmentの寿命はかなり特殊で、プロセスは生きているのに突然UIが再生成されることがある。Mainスレッドは生きているので、スレッドに登録した仕事は完遂するが、それまで参照していたActivityが無くなっているかもしれない。たとえGCで回収されいなくても、その裏で必要な資源は解放されているかもしれない。
コルーチンのいい所は、スコープが破棄されるタイミングで全てのコルーチンがキャンセルされる点にある。ActivityやFragmentはLifecycleScopeを持つので、これを使えばUIを更新する処理でActivityの状態を気にする必要がなくなる。
ただ、コルーチンがスコープによってキャンセルされるということは、実行保証がないことも意味する。重要なイベントが発生したが、画面を回転したばっかりに処理が走らなかった・・・ということはできれば避けたい。LifecycleScopeではなくCoroutineScopeを使えばキャンセルされることはなくなるが、今度はActivityの状態に気を付けないといけない。このように、ドメインロジックをActivityの上で動かすのは意外と悩ましいのだ。
同じ悩みはEventBusのようなメッセージ機構を使った場合でも起こる。ActivityはEventBusからUnsubscribeすることで受信対象から外れる。メッセージは届けられるが、聞き手がいなくなってしまう。
確実に動作させたい処理はCoroutineScopeを使い、Activityの状態に気を付ける。そうでなければLifecycleScopeが便利。
1. 処理がUIを更新するためのもの
LifecycleScopeに紐づけると楽
2. 処理がデータ永続化に関するもの
CoroutineScopeで確実に実行する
3. 永続化した後、UIを更新したい
CoroutineScopeで実行する。完了後、MainThreadにメッセージを投げてUIからデータを読込むか、コルーチンをMainThread上で再Launchする。後者の場合、Activityの状態に気をつける。
4. 複数の処理をキューするもの
重要なら永続化されたキューに、重要でなければ破棄できるキューに追加する。キューから仕事を取り出すスレッドまたはコルーチンが必要。(データが来るまでコルーチンをsuspendするキューをchannelという。)
このように非同期処理の性質を踏まえて、正しい呼び出し方を選ぶのは意外と面倒です。
あと、結果が返ってきていないコルーチンはいつでもキャンセルされると思うこと。トランザクションはデータベース操作にはあるが、データ操作にはない。