8月15日に開催された「Kotlin愛好会 vol.14」に参加してきました。 会場はWantedlyさん!
当日はいつもの感じで始まりました。 天候が崩れなくてよかったー。
Wantedlyさんありがとうございます!#love_kotlin pic.twitter.com/tcpnJpyVI6
— Koji Wakamiya (@D_R_1009) 2019年8月15日
談義について
今回は Dispatchers.Main についてまとめました。
以前に書いた、Androidのメインスレッドの話(非同期の話)をKotlin側から書き直した内容となっています。
Kotlin Coroutinesについて
Kotlin Coroutinesについては完全に省略しました。 今やRoomやRetrofitも対応しているし、概略は大丈夫だろう……! と思っていたのですが、その後に「サーバーサイドで、普段はExecuter使ってスレッド管理してます」などなどの話を聞いたので、ちょっと投げっぱなしすぎたかなと思って反省しています。
Kotlin Coroutinesについては、下記の(日本語訳が)Google Developer記事を最初に参考にすると良さそうです。
とりあえず導入、が終わったらsys1yagiさんとかtakahiromさんとか偉大な先人の記事を読んで触って理解を深めるのが良いかと。
CoroutineDispatchers
今回の談義の下敷きが CoroutineDispatchers です。
https://kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.htmlkotlinlang.org
現時点のstableが 1.2.2 となるので、下記にクラスの参照を貼っておきます。
プラットフォームに対応する CoroutineDispatcher の実装クラス群が Dispatchers となります(この書き方若干語弊がある気がする)。
- JVM
- JS
- Native
Androidでは Default Main IO Unconfined が主に使われますが、このうち Default と Unconfined は他のプラットフォームでも共通です。
IO はJSやNativeには用意されていません。個別で用意するだけの用途がない……のかな? それとも kotlinx.coroutines.io.parallelism の関係なのかしら?
普段はそれぞれの用途をざっくりと理解しておいて launch(Dispatchers.Default) とか withContext(Dispatchers.Main) とかしたりしなかったりします。
viewmodel-ktx の2.1.0から導入される viewModelScope を使うと、ほとんど書くこともなくなります。なお、 viewModelScope は SupervisorJob() + Dispatchers.Main を利用しています。
なお 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においては Looper や Handler あたりが関わってくるのですが、詳細は "Androidを支える技術" がオススメです。
上下巻どちらもオススメですが、今回の話に関連するのは一巻の方です。
Androidにおいては Looper.getMainLooper() することでUIアクセス用のスレッドを取得できます。
Returns the application's main looper, which lives in the main thread of the application.
この処理をKotlin CoroutineのAndroid向けパッケージで行なっています。
Dispatchers は kotlinx.coroutines にありますが AndroidDispatcherFactory は kotlinx.coroutines.android にあります。
先にあげた通りKotlin CoroutinesのMainスレッド実装はプラットフォームにより異なるため、何らかの形でライブラリを組み合わせて使えるようにする必要があるためです。
JSやNativeは Dispatchers.Default がそのまま Dispatchers.Main となるので、差異があるのは JavaFX や Swing 向けの実装となります。
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
ServiceLoaderによる読み込み処理
どのようにして kotlinx.coroutines がプラットフォームに合わせた実装を読み込んでいるかというと、 ServiceLoader クラスを利用しています。
今回初めて知ったクラスです。使い方は下記ブログが簡潔でわかりやすいかなと思いました。
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.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.htmlkotlin.github.io
実際に 1.2.2 では FastServiceLoader というクラスで読み込みを行なっていることが確認できます。
これで Dispatchers.Main がAndroidの世界では Looper.getMainLooer() を指していること、Kotlin Coroutinesが頑張ってマルチプラットフォーム対応をしながらAndroid向けの実装を行なっていることが、何となーくわかるかなと思います。
ServiceLoaderの読み込みが遅い問題とR8
これで探検は終わり……とも行かないので最後のセクションです。 Githubのissueを漁ってみると、次のissueが見つかります。
これに対する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 を使っていました。
- 1.0.0
- 1.1.0
FastServiceLoader は上記の問題に対応するため、追加された対応となります。
よくよくRelease Noteを読んでみると 1.2.0-alpha-2 に対応した旨が記載されています。
とはいえ FastServiceLoader では問題の解決とならず、issue上で議論が続きます。
2018年12月から対応を初めて6ヶ月、ついに根本的な対応が入りました。
R8 1.6.0で追加されたルールにより META-INF で指定されたクラスの最適化が”いい感じ”になるようです。
その結果 ServiceLoader でクラスを読み込む時間が短縮される、とのこと。
いつ対応が終わるの?
- Android Gradle Plugin 3.6.0以上にしたら終わります
R8 の対応は下記のようです。
9a0c2026d99f3c138fafe269c12259e4ac2aeb0f - platform/tools/base - Git at Google
PR作成者曰く(というよりR8/Proguradの開発チームの方っぽいんんですが)、R8 1.6.0から追加されるメカニズムを利用するらしいので、 R8 のバージョンが上がらないと利用できないそうです。
本ブログ執筆時の最新である 1.3.0-rc02 を見てみると、 R8 のバージョンに応じた .pro ファイルが用意されています。
R8のルールを下記のPRで対応。
ということでKotlin Coroutinesのバージョンを引き続きあげ続けることと、Android Gradle Pluginのバージョンもあげ続けることで Dispatchers.Main の最適化がいい感じに進むようです。
終わりに
ちょっと談義を飛ばしすぎたかなーと思ったので詳細を書いておこうと思ったのですが、自分で思っていた以上に情報量が多い話をしていました。 10分程度にまとめたのでかなり端折ってたなーという感想。
R8 はServiceLoader に対して時たまやんちゃをするらしいので、何らかの参考になればとも思います。
自分の関わっているプロジェクトだと、Gsonとの相性が……なので移行にに苦戦中ですしね。
ServiceLoader自分が触ってるプロジェクトで別のところでR8のおかげで死にました 😇 #love_kotlin
— panini (@panini_ja) 2019年8月15日
次に談義するとしたら、もうちょっとマイルドなものにしようっと。 (夏休みの宿題をなんとか8月中に終わらせられた気分)