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月中に終わらせられた気分)