visible true

技術的なメモを書く

リリース前レポート(Firebase Test Lab)で動作しているか判定する

リリース前レポート助かりますよね。Firebase Test Labベースでいろいろ根掘り葉掘りやってくれて最近ではフィードバックもいろいろ充実していて良い感じです

f:id:sys1yagi:20190218164210p:plain

リリース前レポートで動いてるのか判定したい

リリース前レポートいいんですけどロボットがガチャガチャ適当にやるので、あんまり掘ってほしくないルートに行ったりもするんですよ。

たとえば個人アプリでFirebase AuthでGoogleログインをサポートしてたりするんですが、Googleアカウント新規作成みたいなところまでいっちゃうことがよくあって最終的にクラッシュ扱いでレポートが上がってきたりします。

f:id:sys1yagi:20190218164908p:plain:w250
おい

また、広告表示なんかも抑えたいですね。実際に機械によるクリックを防止しろとドキュメントに書いてあったりします。リンクの先はなんか広告側で頑張ってブロックしてね的なドキュメントがあるんですが実施するのは難しい感じで厳しい感じでした。

f:id:sys1yagi:20190218165256p:plain

firebase.test.labフラグを見る

なんとかできないかな〜とつぶやいていたら2年前に自分で実装していたらしい。

完全に忘れてました。ということでFirebase Test Lab and Android Studio  |  FirebaseにあるようにFirebase Test Labの環境で動いているかを次のコードで判定できます。

fun isTestLab(context: Context): Boolean =
    Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"

やったね

おわりに

次は忘れないように書きました。ちばさんありがとう!

java.lang.ClassCastException: com.sun.tools.javac.code.Type$1 cannot be cast to javax.lang.model.type.DeclaredTypeが出たらテストで使うコードがエラーになってるぽい

AndroidX対応を進めていたら自分でつくって自分で使っているFragment生成周りが便利になるhttps://github.com/sys1yagi/fragment-creatorが適切にAndroidX対応できてないぽいことがわかり、いろいろと更新してたらテストで次のエラーがでてこけるようになってしまった。

java.lang.RuntimeException: java.lang.ClassCastException: com.sun.tools.javac.code.Type$1 cannot be cast to javax.lang.model.type.DeclaredType

これはアノテーションプロセッサで型チェックをしている場所で起こる。

https://github.com/sys1yagi/fragment-creator/blob/master/processor/src/main/java/com/sys1yagi/fragmentcreator/model/EnvParser.java#L49

compile-testingで読み込むJavaFileObjectがコンパイルできない場合に起こる

色々調べてみると、https://github.com/google/compile-testingで読み込むテスト用のJavaファイルをロードする時、Javaファイルで使ってるクラスを解決できない場合起こるぽいという事がわかった。設定をいじったから色々変になったのかな〜と思っていたのだけど、テストで使うコードのクラスパスが通ってない結果起こるエラーだとは...。

ということでテストで使うコード(たとえば https://github.com/sys1yagi/fragment-creator/blob/master/processor/src/test/assets/TypeSerializerFragment.java )を見るとandroidx.fragment.app.Fragmentを使うようになっていた。なるほどテストのクラスパスにandroidx.fragment:fragmentを通さないといけないらしい。

先人のコードを参考にする

JSR269周りでAndroidX対応を始めたタイミングとしては大分後発だと思うのできっと先人達がいい感じにしてるに違いないということでいくつかのリポジトリを渡り歩いた。

めちゃくちゃドンピシャだったのがPermissionsDispatcherで、ほぼ全体的に参考にさせてもらった。わいわい。 Replace deprecated "com.google.android" dependency with android.jar · permissions-dispatcher/PermissionsDispatcher@a5e0cac · GitHub

参考になったのは大きく2点

おわりに

AndroidXをサポートした Fragment Creator 2.0.0でました。

Kotlin Coroutine 1.1.0-alpha Change log 全部読む

本エントリは Kotlin Advent Calendar 2018 の記事です。

Kotlin Coroutine 1.1.0-alphaがリリースされましたね。「kotlinx.coroutines/CHANGES.md at master · Kotlin/kotlinx.coroutines · GitHub」Change logを読むことの重要さを思い知ったので全部読みます。

Major improvements in coroutines testing and debugging

コルーチンのテストとデバッグの改善のために新しいモジュールが2つ増えました。

kotlinx-coroutines-debugの追加

ひとつめはkotlinx-coroutines-debugkotlinx.coroutines/core/kotlinx-coroutines-debug at master · Kotlin/kotlinx.coroutines · GitHub

コルーチンの追跡とトレースができるようになります。ByteBuddyを使っているらしい。Androidで使えるかな..? dependenciesをtestImplementation に追加してねというのも気になる。

kotlinx-coroutines-testの追加

もう一つはkotlinx-coroutines-testkotlinx.coroutines/core/kotlinx-coroutines-test at master · Kotlin/kotlinx.coroutines · GitHub

Dispatchers.setMain関数などを提供してくれるらしい!これは助かる。

スタックトレースの改善

System.setProperty("kotlinx.coroutines.debug", "") ってセットしておくと、dispatcherを跨いでもスタックトレースで追えるようになる。これは助かる。

Basic stacktrace augmentation · Issue #493 · Kotlin/kotlinx.coroutines · GitHub

その他の改善

MainScopeファクトリとCoroutineScope.cancel関数を追加

https://github.com/Kotlin/kotlinx.coroutines/issues/829

MainScopeファクトリの中身がCoroutineScope(Dispatchers.Main + SupervisorJob()) で、root scopeではSupervisorJob使っていこうなってことなのかな〜うーんという感じ。CoroutineScope.cancelは便利そう。

resumeWithExceptioncancelのCancellableContinuationの競合が解決された。キャンセル中の例外は例外ハンドラに伝搬しなくなった。

特に感想なし

Dispatchers.DefaultJVM上のCPU消費量を削減

https://github.com/Kotlin/kotlinx.coroutines/issues/840

CoroutineSchedulerが処理がない時に余計に計算してたらしい。改善されてよかった

初期化されてないDispatcherをより素早く診断して検出

https://github.com/Kotlin/kotlinx.coroutines/issues/880

Dispatchers.Main使ってるのにandroid dependencies入れてないみたいな時にハングするケースがあったらしい。直ってよかった。普通に使ってる分にはあんまり関係なさそう。

Conflated channelが線形化可能になった

どういうこと〜?

コルーチンの結果の型がDisposableHandle だったときに起こる問題を修正

https://github.com/Kotlin/kotlinx.coroutines/issues/835

ハングっちゃうらしい。普通に使ってる分には関係なさそう。

Dispatchers. JavaFx の初期化のバグを修正

https://github.com/Kotlin/kotlinx.coroutines/issues/816

withTimeout関数のバグを修正

https://github.com/Kotlin/kotlinx.coroutines/issues/870

タイムアウトを負の数にすると、TimeoutCancellationExceptionじゃなくてCancellationExceptionが飛んでしまう問題を修正したらしい

Kotlin/Native でシングルスレッドワーカーをサポート

Kotlin/Nativeでもコルーチンが便利に使えるらしい。

JavascriptDispatchers.Defaultでjsdomをサポートした

あんまりよくわかってない

rxFlowableの型パラメータがAny で制限された

あんまりよくわかってない

Guava 27をサポート

kotlinx-coroutines-guava

コルーチンをプログレッシブモードでビルドするようになった

Kotlin 1.2.50とかで入ったモードっぽいけど詳細はわからない

ドキュメントをいろいろ修正

わいわい

感想

意外としれっといろいろ入るんだな〜。おもしろい。

Androidと非同期処理 とCoroutine1.0.0

Google Play App Dojoで話してきました。Google Play App Dojoは非公開イベントなので全体的な内容は言えないんですが、どの発表もおもしろい & 濃いので、もし参加のチャンスがあったらぜひ行ったほうがいいな〜と思いました。

Androidと非同期処理 とCoroutine1.0.0

資料はコチラ

話した事

f:id:sys1yagi:20181219102256p:plain

詳細は資料を見て下さい。

その他リンクなど

Android Suspenders

Android Dev Summit '18のコルーチンのセッションです。資料読んだあと見るとわかりが深まると思います。Androidと非同期処理 とCoroutine1.0.0 では触れられなかったところ(Android APIをコルーチン化する、複数回値を返却するケースなどなど)が結構あるのでぜひ見て下さい。 www.youtube.com

資料内でリンクしてるやつ

おわりに

SupervisorJobとsupervisorScope関数についてはAndroidでは不要だろうなと思っているけどもう少し調べたらブログ書こうかなと思います。だいじなことなので二回書きますがGoogle Play App Dojoどの発表もおもしろい & 濃いので、もし参加のチャンスがあったらぜひ行ったほうがいいな〜と思いました。

Kotlin Coroutine 0.26.0 Structured concurrencyってなんなの

Structured concurrency – Roman Elizarov – Mediumでは、Kotlin coroutine 0.26.0でCoroutineScopeを導入した経緯などが書かれているんですが、タイトルのStructured concurrencyってのが結局なんなのか本文中にはあんまり出てこないんですよね。

んで、CoroutineScopeの動作の理解に精一杯で、coroutineScope関数がなんなのか、どういう時に使うべきなのかよくわからないし、ましてやsupervisorScope関数なんて言われた日には意味が不明なわけです。 とりあえずlaunch関数内でasync関数呼び出したら駄目なんだな〜ふ〜んなんで?

Notes on structured concurrency, or: Go statement considered harmfulを頑張って読む

Structured concurrency – Roman Elizarov – MediumのFurther readingにNotes on structured concurrency, or: Go statement considered harmful — njs blogを読むことを強く勧める!と書いています。パッと開くと超長いので心が折れかけるんですが読んでみたら面白かったのでメモします。

並行性の構造化

Structured concurrencyは概ね 並行性の構造化 って訳すとよさそうです。 構造化プログラミングはgotoを駆逐するために考えられたものですが、並行性の構造化は並行処理におけるgoto的なものを駆逐するための議論みたいです。 構造化プログラミングは順次, 反復, 分岐の構造を組み合わせてプログラム書くことでgotoは無くせるしむしろいい感じになるぜって話ですね。

並行性の中のgoto

エントリではgo statement(goroutineの起動)はgotoみたいなもんじゃ!って言ってます。go statementに限らず並行処理を起動するものはどれも同じみたいです。 たとえばtry-with-resources(これはJava)の中でスレッドを起動してその中でリソースにアクセスしたらどうなるか?ぶっ壊れます。 gotoじゃねえか!ということだそうです。でも確かにわかる気もする。

Trioの例

https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/#nurseries-a-structured-replacement-for-go-statements では、並行性についても構造化プログラミングと同じように並行性の構造のモデルを定義して取り入れるといいんじゃないか的な話と実際にpythontrioというライブラリの說明が始まります。

とりあえず図としてはこれがわかりやすいんじゃないかと思います。ほかのパターンもあるのかはちょっと分からないですがCoroutineScopeを理解するには十分かなと思います。

f:id:sys1yagi:20181212220527p:plain:w300

fork/joinに似てますけど実際に影響を受けてるらしいです。つまりどっから並行性が始まってどこで終わるんや、ってことを明示的に書けってことですね。これCoroutineScopeですね。こういう感じ。

suspend fun startParallel() = coroutineScope { // 並行性の分岐の根本
    // 3並行に分かれる
    val a = async { requestA() }
    val b = async { requestB() }
    val c = async { requestC() }

    // 合流する
    a.await() + b.await() + c.await()
}

タイミングがずれるケースとか複雑に分岐するケースとかもあるとおもうんですが、fork/joinの根本と合流がcoroutineScopeで適切に切られていればOKってことですね〜

例外の伝搬とCoroutineScope

例外の伝搬についてもfork/joinの図がわかりやすいですね。分岐の何処かが死んだら全体が即座に死ぬ。並行実行の単位がスコープってことですかね〜。 launch関数内でスコープを切らずにasync関数呼び出したら駄目なのは、async関数が並行性の分岐を表すからで、スコープを切らない場合はforkの根本がlaunch関数の親のスコープになるから、と說明できそうです(図を書いてみたもののカチッとしなかったので省略)。

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
// asyncがエラーになるとscopeがキャンセルされる
scope.launch {
  val a = async { requestA() }.await()
}

終わりに

とりあえず雑ですがメモです。大分わかってきました。まだ理解できてないとこもありそうですが.. ざっくりしか読んでないからまた何度か読みます。 Notes on structured concurrency, or: Go statement considered harmful — njs blog

Roomはどのようにsuspend関数を実現しているのか

Roomに2.1.0-alpha03からsuspend関数(コルーチン)のサポートが入りましたね。ちょっと気になったのでいくらか触ってみました。

Architecture Components Release Notes  |  Android Developers

f:id:sys1yagi:20181209155920p:plain

何ができるようになるのか

DAOでsuspend関数を宣言できるようになりました。具体的には次です。

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAllUsers(): List<User>

    @Delete
    suspend fun delete(user: User)
}

CoroutineScopeのなかでこれらの関数を呼ぶとノンブロッキングで各種操作ができるようになります。

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
scope.launch {
    try {
        val dao = db.userDao()
        0.until(100).forEach {
            dao.insert(User())
        }
        val size = dao.getAllUsers().size // 100
        // do something
    } catch (e: java.lang.Exception) {
        // error
    }
}

便利ですね。

RoomはJavaコードを生成するライブラリ

さて、そこで気になるのは、Kotlinオンリーの機能であるはずのsuspend関数をRoomはどのように生成しているのだろう?ということです。実際に生成されたコードを読んでみました。

Continuationを明示的に受け取る関数をJavaで実装している

例えば、suspend fun insert(user: User)の生成コードは次のようになっています。

@Override
public Object insert(final User user, final Continuation<? super Unit> p1) {
    return CoroutinesRoom.execute(__db, new Callable<Unit>() {
        @Override
        public Unit call() throws Exception {
        __db.beginTransaction();
            try {
                __insertionAdapterOfUser.insert(user);
                __db.setTransactionSuccessful();
                return kotlin.Unit.INSTANCE;
            } finally {
                __db.endTransaction();
            }
        }
    }, p1);
}

第二引数にfinal Continuation<? super Unit> p1 !! なるほどCPSで渡されるContinuationを明示的に受け取る関数を宣言しています。これでsuspend関数として呼び出される関数をJavaでも宣言できるんですね〜すごい。

CoroutinesRoom objectによるブリッジ

次に注目なのはinsert関数の冒頭で呼び出しているCoroutinesRoom.execute関数です。この中身を覗くと、次のようになっています。

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class CoroutinesRoom {
    companion object {
        @JvmStatic
        suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
            return withContext(db.queryExecutor.asCoroutineDispatcher()) {
                callable.call()
            }
        }
    }
}

@JvmStaticを宣言してJavaからstatic関数として呼び出せるようにしています。CoroutinesRoomはKotlinなのでsuspendキーワードが使えます!そこでwithContext関数を呼び出してqueryExecutorを使ってクエリを実行しています。suspend関数をJavaで実装して、さらにKotlinに引き渡してwithContext関数を使って実行を別のスレッドに切り替えつつ中断/再開を実現しています。

呼び出し元を見直すと次のようになっています。

CoroutinesRoom.execute(__db, new Callable<Unit>() {...省略...}, p1)

p1はContinuationです。JavaからKotlinのsuspend関数を呼び出すには末尾にContinuationを明示的に渡せばいいんですね〜すごい。

withContextとExecutor

withContext - kotlinx-coroutines-core

withContext関数は指定したContextで渡したラムダ式の中断/再開を行います。Roomではdb.queryExecutorをCoroutineDispatcherに変換して渡してます。デフォルトだと自動的にRoomが用意したExecutorが使われてしまうのですが、queryExecutorはRoomDatabase.Builderでセットできます。

次の例はDispatchers.IOをRoomのqueryExecutorにセットする例です。

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "database-name"
        ).apply {
            // ※良い方法ではない気がするのでマネはしないほうがいいかも
            val dispatcher = Dispatchers.IO
            if (dispatcher is ExecutorCoroutineDispatcher) {
                setQueryExecutor(dispatcher.executor)
            }
        }.build()

すごいぞRoom

コルーチンを主に使っているのでこういったサポートはとてもたすかります。 あとJavaによるsuspend関数の橋渡しのアイデアはすごいと思った。わいわい。

Kotlin Coroutine 1.0.0までに夏から変わったところ

日進月歩。夏に話した頃とは大きく変わってしまったKotlinコルーチン。

Kotlin Fest 2018でコルーチンの話をしてきた - visible true

↑はコルーチンの概念と実現の仕組みを中心に置いたので、その辺はまぁ変わってないんで問題ないんですが、後半の実際にコルーチンを使う場面に関してはすでにdeprecatedっていうか、そのまま書いても動かないくらい変化してしまいました。

ということでざっくりメモ

0.25.xから1.0.0までの主要な変化

基本的にはリリースノートと関連するissueの議論を読めば大体わかります。 https://github.com/Kotlin/kotlinx.coroutines/releases

[0.26.0] CoroutineScopeを導入し既存の書き方がdeprecatedに

9/12にリリース。kotlin festから2週間やん。

CoroutineScopeという概念が追加された。既存のコルーチンはすべてGlobalScopeとして扱うことになった。そのほか構造的並行性(Structured concurrency)が基本的な考え方となり、スコープの階層を適切に構築しようなみたいな話になったぽい。

もともとは全部グローバルコルーチンだったので単純だけどリークの可能性もあった(永遠に生きるコルーチンとかのケアがあんまりなかった)が、構造的並行性によってrootスコープから末端まで親子関係を適切に構築することでライフサイクルの伝搬をいい感じにしていこうなという感じ。

parent jobで親子関係をやりくりしていたのを、CoroutineScopeでデフォルトで必ず親子になるようにしたため、書きやすくなったと言えるし設計もしやすくなったなと思う。大変だけど。

ちょっと追記

CoroutineScopeによってコルーチンのライフサイクルを構築しやすくなった。これによってAndroidとの親和性が上がったと言える。ActivityのライフサイクルやFragmentのライフサイクルなどに合わせてそこにぶら下がるコルーチン全体を停止できるようになるから。なのでコルーチンをガシガシ書ける空間(Activity, Fragment, ViewModelなどのライフサイクルに合わせて動くCoroutineScopeを実装できる)を提供できて、アプリに導入するハードルが下がる。実際CoroutineScope導入後にJetpackコンポーネントでどんどんコルーチンサポートが入ってきている。この点はすごく良かったのでは〜って思う。

[0.26.0] Dispatchersを導入

CoroutineScopeの動きとは別でDispatcherの名前がバラバラ(CommonPool, UI, JavaFx...)なのでなんとかしない?みたいな議論のなかでDispatchersオブジェクトが導入され、そこに生やす形になった。

  • CommonPool -> Dispatchers.Default
  • UI -> Dispatchers.Main
  • _ -> Dispatchers.IO

など。まぁこれは名前変わったね程度の理解で大体よさそう。CommonPoolはいずれなくなるらしいけどまぁよしなにやってくれるみたいなので頼むみたいな感想。

[0.30.0] 例外伝搬の仕様が変わった

9/29リリース。矢継ぎ早〜

コルーチンが例外を投げたときにどこまでどう伝搬するかみたいな話。たとえば次のコード

suspend fun foo() = coroutineScope<Unit> {
  val one = async {
    delay(Int.MAX_VALUE) // suspend until cancellation
  }

  val two = async { throw MyException() }

  one.await()
}

oneとtwoが同時に動きだすけどtwoはすぐに例外を起こす。しかしDeferredはawait()しないと例外が飛ばないので、oneが長いとtwoが死んでるのにずっと待っちゃうみたいな動きは問題だ、ってことで、即時に親をキャンセルする形になった。

これが怖いのは次ようなコードは書いちゃ駄目になったということ

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
scope.launch {
  try {
    val a = async { // launchの中でasync起動したらあかん
      delay(1000)
      throw Exception()
    }.await()
  } catch (e: java.lang.Exception) {
    e.printStackTrace()
  }
}

launchとasyncは親子関係を持っているがasyncは親をcancelにするのでlaunchもcanceledになり、さらにその上まで例外を伝搬する。アプリでこういう呼び出しをしていると例外補足できずにクラッシュしてしまう。

明確な理由はわからないが、スコープビルダー(coroutineScope{})は親をキャンセルしないことになっていて、次のように書けば解決する。

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
scope.launch {
  try {
    // ※クラッシュしないけどこのケースでは非推奨なのでこれは使わないでください
    coroutineScope { 
      async {
        delay(1000)
        throw Exception()
      }
    }.await()
  } catch (e: java.lang.Exception) {
    e.printStackTrace()
  }
}

ただほとんどのワンショット継続はwithContext(Dispatchers.IO)でまかなえるので次に書くようにしたほうがよさそう

val job = Job()
val scope = CoroutineScope(Dispatchers.Main + job)
scope.launch {
  try {
    withContext(Dispatchers.IO) {
        delay(1000)
        throw Exception()
    }
  } catch (e: java.lang.Exception) {
    e.printStackTrace()
  }
}

coroutineScope{}の使い所はスコープを切り換える時、つまりそのスコープの中で別のコルーチンを起動する時。async複数呼び出して並行で実行しまとめて待ち合わせる場合はcoroutineScope{}を使う必要がありそう。

この辺ややこしいし言語化するの大変なので大幅に省くけど、さらにSupervisorという考え方も導入されて混乱に拍車がかかっている。Supervisorはスコープ内のコルーチンがエラーになっても親がキャンセルされずに、他の子コルーチンを継続するやつ。実際使う機会はAndroidではほとんどないよな〜というのが個人的な見解。並行で複数リクエストするけど投機的で相互の失敗に依存しないケースとか。

[1.0.0] experimental packageを廃止 🎊

10/30 🎊

https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.0.0

experimentalパッケージからはずれてkotlinx.coroutinesになった!めでたい。 マイグレーションも1.3マイグレーションの中でやってくれるので移行も楽。楽しい。

おわりに

リリースノートはしっかり読んだほうがよいですね 😵