fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

たくさんのTextViewをアニメーションさせる

画面に20個ぐらいアニメーションするビューがあってなんとかして60fps出すためにいろいろやったことをメモしておく。

エミュレーターでやってたのが全部悪い(実環境で動かしたらわりとすんなり出た)というオチなので参考になるかはわからない。

前提

  • ValueAnimatorAnimatableDrawableKeyFrameで波紋のようなアニメーションを周期的に動かす。これはせいぜい数個
  • ValueAnimatorTextViewのtextColor(白と黒の間のグレースケール)を周期的に変えていく。これは20個ぐらいある

方針

Debug.startMethodTracing()で数フレーム分観測し、いらない処理を見つけてこれが実行されないように削っていく。この資料の38Pから数ページが参考になる。

AndroidVitals徹底活用 - Speaker Deck

最初のほうはGCも頻繁に起きていたのでAndroid Studioのメモリアナライザも使ったりした。

ポイント

ValueAnimatorでアニメーションをやるときは次のメソッドを実装することになると思うが、この中の処理を軽くしたり、中で新しいオブジェクトを作らないようにする。

  • TypeEvaluator.evaluate()
    • SDKのクラスを使うときは中で何をやっているか確認する。例えばArgbEvaluatorは最初にセットしたColor Intがevaluate()に渡される時にはIntegerのオブジェクトになってしまう。
  • AnimatorUpdateListener.onAnimationUpdate()
  • Drawable.draw()

やったこと

オブジェクト生成の抑制

  • listOf()とかarrayOf()とかで作っていたKeyFramevalueの配列をfloatArrayOf()にする。これによりTypeEvaluatorに値を渡すときのFloatオブジェクトの生成を抑制できる
  • FloatTypeEvaluatorにキャッシュ用の配列を渡して初期化する。これをしないとフレームごとにfloat[]が作られてしまう
  • 配列操作の処理でmapとかfilterを使っているところをfor文に置き換える。FloatArrayでmapとかを呼ぶとIteratorがフレームごとに作られてしまうしnext()がちょっと気になるくらい遅い。また、Javaコードにデコンパイルしてコードを見ると、mapに渡した関数がクラスになっていてこれのオブジェクトも毎フレーム生成されていた。

描画系

  • すべてのTextViewの背景にAnimatableDrawableをセットして、動かすときになったら表示させるようにしていたがこれをやめて、アニメーションさせるときにはじめてセットする。Drawableがセットされていたら更新しようとするのでこれをやめさせる。そこそこ効いた。
  • WRAP_CONTENTTextViewGravity.CENTERを指定していたのをはずす。テキストアライメントがNORMAL以外だとBolingLayout.draw()の中で毎回描画領域の再計算が行われてしまうのでこれをやめさせる。NORMALになっていると即座にCanvas.drawText()が実行される。そこそこ効いた。
  • ヒントテキストもリンクも必要ないので、hintTextColorlinkTextColorにnullをセットしておく。これをやっておくとTextView.setTextColor()をやった時にhintTextColorとかlinkTextColorも更新しようとするのを防げる。超細かい。
  • TextView.setTextColor(int)ではなくTextView.setTextColor(ColorStateList)のほうを使う。ColorIntを渡すほうは内部的にColorStateList.valueOf(int)を呼んでColorStateListに変換しているのだが、内部的にsynchronizedで囲われているせいか時々めっちゃ遅いことがある。グレースケールなら長さ256の配列に収まるのでアプリ起動時に作ってこれを使うようにする。超細かい。
  • 同じタイミングで動く複数のDrawableを別々のValueAnimatorで制御していたが、これをやめて一つのValueAnimatorのなかで全部のDrawableのinvalidateをやることにした。結構効いた。
  • Drawable.invalidateSelf()をやめてView.postInvalidateInAnimation()を使うようにした。そこそこ効いた。

カジュアルにIdlingResourceを使う

  • UIテストをやる時にMockWebServerを使ってサーバの挙動をモックしていると、レスポンスをちゃんと待ち構えないとテストが失敗することが稀によくある*1
  • カジュアルにIdlingResourceを使ってカジュアルに待てるようにしたい
  • テストを書くためのハードルを下げたい

ポイント:

  • ある状態になるまで待ちたくなった時にregisterして所望の状態になったらすぐにunregisterする。そうせず大域的に使うといろんな条件がバッティングして失敗する。
  • registerしたらなにがなんでもunregisterされるようにしたいのでtry (catch) finallyでくくる
  • isIdleNow()のなかでActivityLifecycleMonitor.getActivitiesInStage(Stage)を使ってactivityを取得できれば勝確
    • ActivityScenarioとかActivityTestRuleとかをわたしてもよいかも?
    • 結構無茶なことをしているしIdlingThreadPoolExecutorとかを使ってうまくやるのがいいと思う
    • ダイアログとかRecyclerViewの状態を待ちたいときはこうするしかないような気もするがそもそもこんなことしなければならないのが間違っている気がする

コード例:

まずはIdlingResources。結局はisIdleNow()の挙動を実装できればいい。公式サイトにはisIdleNow()の中でIdlingResource.ResourceCallback.onTransactionToIdle()を呼ぶなと書いてあるが今回の用途ではここで呼ばないとテストが進まなくなることがあるので仕方なく呼ぶ。

fun createIdlingResource(name: String, block: () -> Boolean): IdlingResource {
    return object : IdlingResource {
        override fun getName() = name

        override fun isIdleNow(): Boolean {
            val isIdle = block()
            if (isIdle) {
                callback?.onTransitionToIdle()
            }
            return isIdle
        }

        private var callback: IdlingResource.ResourceCallback? = null
        override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
            this.callback = callback
        }
    }
}

fun waitWithIdlingResource(name: String, block: () -> Boolean, afterTask: () -> Unit) {
    val idlingRegistry = IdlingRegistry.getInstance()
    val resource = createIdlingResource(name, block)
    try {
        idlingRegistry.register(resource)
        afterTask()
    } finally {
        idlingRegistry.unregister(resource)
    }
}

先ほどのメソッドを作ってテスト対象のActivityが所定のStageにIdlingResourceを作ってみる。

inline fun <reified T : Activity> waitForActivity(stage: Stage, noinline afterTask: () -> Unit) {
    val name = "wait_for_${T::class.java.simpleName}_in_${stage.name}"
    waitWithIdlingResource(name, {
        ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull {
            it is T
        } != null
    }, afterTask)
}

先ほどのwaitForActivityをリファクタリングしてFragmentが差し込まれるまで待つIdlingResourceを作ってみる。

inline fun <reified T : Activity> waitForActivity(
    stage: Stage = Stage.RESUMED,
    name: String = "wait_for_${T::class.java.simpleName}_in_${stage.name}",
    crossinline onActivity: (T) -> Boolean = { true },
    noinline afterTask: () -> Unit
) {
    waitWithIdlingResource(name, {
        val a = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull {
            it is T
        } ?: return@waitWithIdlingResource false
        return@waitWithIdlingResource onActivity(a as T)
    }, afterTask)
}

inline fun <reified T : Fragment> waitForFragment(noinline afterTask: () -> Unit) {
    waitForActivity<FragmentActivity>(name = "wait_for_${T::class.java.simpleName}",
        onActivity = { a ->
            a.supportFragmentManager.fragments.firstOrNull {
                it is T
            } != null
        },
        afterTask = afterTask)
}

しなくていいならしないに越したことはないと思うけど、退っ引きならない事情でどうしてもやらなければならない時だけどうぞ。Activityを待つ奴を応用するとRecyclerViewに所望のViewが差し込まれるまで待つとかいうのも作れるようになるので興味のある方は挑戦してみてください。


最近読んだのでよかったらどうぞ。

peaks.cc

*1:結構すぐ返ってくるので待ってなくても割と成功する

Androidでマルチモジュールをやってみているという話

droidkaigiでマルチモジュール関連の発表をたくさん聞いて、やってみたくなったのでやってみている。

github.com

データレイヤーを単に外に出すところまではなんとなくできたけどそこから先のところでうまく行かなかったり腑に落ちたり落ちなかったりしている。基本的な設計力がないからマルチモジュール化できないということなのであろうか。どうしてそうなっているかを理解せず何かの真似でやってみてもうまく行かない。

ちなみにここでモジュールって言ったらGradle(とかKotlin)で言う所のモジュールのことです。

daggerがなんか難しい

  • モジュールとSubcomponentとを対応させればいいんじゃね?と考えて公式ページを見ながらやってみるものの説明の通り(?)にやってみたらStackOverflowError
  • ComponentをもっているモジュールがSubcomponentの依存関係に引っ張られて本来不要なモジュールに依存せざるを得なくなっている*1
    • kaptの挙動がよくないという説がある様子
    • apiのモジュールを挟めばいいんだろうか
  • koinとかはどうなんだろうと思い始める

スタイルとかテーマって開発中どうやって確認すればいいんだろう

  • マテリアルデザインのテーマが読み込めなくてFABを表示できない(?)何が悪いのかもよくわかっていない
  • styleとかthemeはsharedなモジュールに置く?スタイルと実装とは切り離して考えるべきだとも思うけどそこまでやるのもなんか違う気がしている。

Activity, Fragment, ViewModelはどう分けるのがよいのか

  • ViewModelはViewに依存しないとはいっても、どんなViewDataBindingにバインドされるかは知っているはず*2ではないかと思うので、レイアウトファイルとViewModelはなんとなくセットにしておきたい
    • なんだかんだ言って画面はFragmentを組み合わせて作ることになるので、Fragmentのレイアウトファイルと、それとバインドするViewModelとをセットにして作ることになるはず
    • そう考えるとFragmentとViewModelは一緒のモジュールに入れればいいんじゃないのか?と思わなくもない。でもモジュール化の意義的に正しいのか、なんとなく違和感を覚える*3
  • 一方で、ActivityのレイアウトファイルはFragmentのコンテナしか置かないとかいうやつがでてくる*4
    • そういうやつはバインドするデータやビューがあんまりないのでViewModelとバインドできてもおいしくない
    • そういうActivityはFragmentの置き換えを実装するだけでいいという感じになる。どんな時にどのFragmentを置くかというのをViewModelで管理することになるかもしれないが、FragmentManagerをいじるロジックはActivityに書くことになり画面が増えてくるとつらい
  • Fragmentの置き換えを専門にやるクラスがあればよいのか?→JetpackのNavigationがそれにあたるか
    • navigationを使えば、ActivityやFragmentは他にどんなActivityやFragmentがあるのか知らなくてよさそう
    • navigationだけがActivityやFragment、他のnavigationを知っていればよいというふうにできれば、navigation(の具象クラス)はapplicationと同じモジュールに置けばよい
    • 画面のことはenumとかsealed classで定義したクラス(sharedなモジュールに置く)で表現すれば、navigationの抽象クラスしか知らないクラスの中でも画面遷移を実現できそう
    • 画面遷移イベントの発行を行う処理はViewModelでやってよい
      • 画面遷移を依頼するようなイベントオブジェクトを定義するとMVIになるのか?

先は長い。俺たちの戦いは始まったばかりだ。

*1:build.gradleのdependenciesに列挙しなければならないという意味で

*2:特定のViewDataBindingにバインドされるのを前提にして作るはず

*3:ViewModelからFragmentを見えなくしたいからモジュール化するのかなとも思う

*4:ボトムナビゲーションとかドロワーとかがあるやつは知らん