Continuity is The Father of Success

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

Kotlin愛好会 vol.14でDispatchers.Mainについて談義してきました

8月15日に開催された「Kotlin愛好会 vol.14」に参加してきました。 会場はWantedlyさん!

love-kotlin.connpass.com

当日はいつもの感じで始まりました。 天候が崩れなくてよかったー。

談義について

今回は Dispatchers.Main についてまとめました。

speakerdeck.com

以前に書いた、Androidのメインスレッドの話(非同期の話)をKotlin側から書き直した内容となっています。

blog.dr1009.com

Kotlin Coroutinesについて

Kotlin Coroutinesについては完全に省略しました。 今やRoomやRetrofitも対応しているし、概略は大丈夫だろう……! と思っていたのですが、その後に「サーバーサイドで、普段はExecuter使ってスレッド管理してます」などなどの話を聞いたので、ちょっと投げっぱなしすぎたかなと思って反省しています。

Kotlin Coroutinesについては、下記の(日本語訳が)Google Developer記事を最初に参考にすると良さそうです。

developers-jp.googleblog.com

developers-jp.googleblog.com

developers-jp.googleblog.com

とりあえず導入、が終わったらsys1yagiさんとかtakahiromさんとか偉大な先人の記事を読んで触って理解を深めるのが良いかと。

sys1yagi.hatenablog.com

CoroutineDispatchers

今回の談義の下敷きが CoroutineDispatchers です。

kotlinlang.org

現時点のstableが 1.2.2 となるので、下記にクラスの参照を貼っておきます。

kotlinx.coroutines/CoroutineDispatcher.kt at 1.2.2 · Kotlin/kotlinx.coroutines · GitHub

プラットフォームに対応する CoroutineDispatcher の実装クラス群が Dispatchers となります(この書き方若干語弊がある気がする)。

Androidでは Default Main IO Unconfined が主に使われますが、このうち DefaultUnconfined は他のプラットフォームでも共通です。 IO はJSやNativeには用意されていません。個別で用意するだけの用途がない……のかな? それとも kotlinx.coroutines.io.parallelism の関係なのかしら?

普段はそれぞれの用途をざっくりと理解しておいて launch(Dispatchers.Default) とか withContext(Dispatchers.Main) とかしたりしなかったりします。 viewmodel-ktx の2.1.0から導入される viewModelScope を使うと、ほとんど書くこともなくなります。なお、 viewModelScopeSupervisorJob() + Dispatchers.Main を利用しています。

medium.com

なお Dispatchers.Default ではなく Dispatchers.Main を利用しているのは、下記の理由です。 「UIスレッドを制御する ≒ ViewModelはUIに関連する処理を受け取る」ので、そもそも Dispatchers.Main で受け取らない理由がないと自分は理解しています。

Dispatchers.Main is a natural fit for this case since ViewModel is a concept related to UI that is often involved in updating it so launching on another dispatcher will introduce at least 2 extra thread switches. Considering that suspend functions will do their own thread confinement properly, going with other Dispatchers wouldn’t be an option since we’d be making an assumption of what the ViewModel is doing.

Dispatchers.Main とは

閑話休題、というかここからが本題。 Dispatchers.Main の探検です。

Androidにおいては LooperHandler あたりが関わってくるのですが、詳細は "Androidを支える技術" がオススメです。 上下巻どちらもオススメですが、今回の話に関連するのは一巻の方です。

Androidを支える技術〈I〉──60fpsを達成するモダンなGUIシステム (WEB+DB PRESS plus)

Androidを支える技術〈I〉──60fpsを達成するモダンなGUIシステム (WEB+DB PRESS plus)

Androidにおいては Looper.getMainLooper() することでUIアクセス用のスレッドを取得できます。

Returns the application's main looper, which lives in the main thread of the application.

Looper  |  Android Developers

この処理をKotlin CoroutineのAndroid向けパッケージで行なっています。

kotlinx.coroutines/HandlerDispatcher.kt at 1.2.2 · Kotlin/kotlinx.coroutines · GitHub

Dispatcherskotlinx.coroutines にありますが AndroidDispatcherFactorykotlinx.coroutines.android にあります。 先にあげた通りKotlin CoroutinesのMainスレッド実装はプラットフォームにより異なるため、何らかの形でライブラリを組み合わせて使えるようにする必要があるためです。

JSやNativeは Dispatchers.Default がそのまま Dispatchers.Main となるので、差異があるのは JavaFXSwing 向けの実装となります。

kotlinx.coroutines/ui at 1.2.2 · Kotlin/kotlinx.coroutines · GitHub

MainスレッドとUIスレッド

Annotationのドキュメントを確認すると @MainThread@UIThread は交換可能なAnnotationとして扱われるそうです。 ただし、下記の通りの注釈がついていることから本ブログでは Mainスレッド と(気がつく限り)表記しています。

Note: Ordinarily, an app's main thread is also the UI thread. However, However, under special circumstances, an app's main thread might not be its UI thread; for more information, see Thread annotations.

MainThread  |  Android Developers

UiThread  |  Android Developers

ServiceLoaderによる読み込み処理

どのようにして kotlinx.coroutines がプラットフォームに合わせた実装を読み込んでいるかというと、 ServiceLoader クラスを利用しています。

docs.oracle.com

今回初めて知ったクラスです。使い方は下記ブログが簡潔でわかりやすいかなと思いました。

unageanu.hatenablog.com

META-INF で読み込みたいクラスを指定しておき ServiceLoader.load することで呼び出せるようになるそうです。 下記の通り各JVMプラットフォームで META-INF を指定しています。

Dispatchers.Main のDocにも ServiceLoader で読み込むことが明記されています。

On JVM it either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the ServiceLoader.

kotlin.github.io

実際に 1.2.2 では FastServiceLoader というクラスで読み込みを行なっていることが確認できます。

kotlinx.coroutines/MainDispatchers.kt at 1.2.2 · Kotlin/kotlinx.coroutines · GitHub

これで Dispatchers.MainAndroidの世界では Looper.getMainLooer() を指していること、Kotlin Coroutinesが頑張ってマルチプラットフォーム対応をしながらAndroid向けの実装を行なっていることが、何となーくわかるかなと思います。

ServiceLoaderの読み込みが遅い問題とR8

これで探検は終わり……とも行かないので最後のセクションです。 Githubのissueを漁ってみると、次のissueが見つかります。

github.com

これに対するJakeの回答がこちらです。(あらかじめ書いておきますが、この問題は2018年12月9日当時のものです)

ServiceLoader should definitely be avoided on Android. I filed https://issuetracker.google.com/issues/120436373 recently to rewrite it in release builds.

Slow android Dispatchers.Main init · Issue #878 · Kotlin/kotlinx.coroutines · GitHub

当時のバージョンでは FastServiceLoader ではなく ServiceLoader を使っていました。

FastServiceLoader は上記の問題に対応するため、追加された対応となります。

github.com

よくよくRelease Noteを読んでみると 1.2.0-alpha-2 に対応した旨が記載されています。

github.com

とはいえ FastServiceLoader では問題の解決とならず、issue上で議論が続きます。 2018年12月から対応を初めて6ヶ月、ついに根本的な対応が入りました。

github.com

R8 1.6.0で追加されたルールにより META-INF で指定されたクラスの最適化が”いい感じ”になるようです。 その結果 ServiceLoader でクラスを読み込む時間が短縮される、とのこと。

いつ対応が終わるの?
  1. Android Gradle Plugin 3.6.0以上にしたら終わります

FastServiceLoader performs I/O on calling thread and prevents R8 optimization · Issue #1231 · Kotlin/kotlinx.coroutines · GitHub

R8 の対応は下記のようです。

9a0c2026d99f3c138fafe269c12259e4ac2aeb0f - platform/tools/base - Git at Google

PR作成者曰く(というよりR8/Proguradの開発チームの方っぽいんんですが)、R8 1.6.0から追加されるメカニズムを利用するらしいので、 R8 のバージョンが上がらないと利用できないそうです。 本ブログ執筆時の最新である 1.3.0-rc02 を見てみると、 R8 のバージョンに応じた .pro ファイルが用意されています。

kotlinx.coroutines/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools at 1.3.0-rc2 · Kotlin/kotlinx.coroutines · GitHub

R8のルールを下記のPRで対応。

github.com

github.com

ということでKotlin Coroutinesのバージョンを引き続きあげ続けることと、Android Gradle Pluginのバージョンもあげ続けることで Dispatchers.Main の最適化がいい感じに進むようです。

終わりに

ちょっと談義を飛ばしすぎたかなーと思ったので詳細を書いておこうと思ったのですが、自分で思っていた以上に情報量が多い話をしていました。 10分程度にまとめたのでかなり端折ってたなーという感想。

R8ServiceLoader に対して時たまやんちゃをするらしいので、何らかの参考になればとも思います。 自分の関わっているプロジェクトだと、Gsonとの相性が……なので移行にに苦戦中ですしね。

次に談義するとしたら、もうちょっとマイルドなものにしようっと。 (夏休みの宿題をなんとか8月中に終わらせられた気分)