RecyclerViewのようなものを作ろうとしたけどうまくいかなかった
仕事で作ったクラスの構造がよくなかったのでそのその反省を少しまとめてみる。
Adapter
とは別にLayoutManager
的なものも作る必要があったので、RecyclerView
の構造を参考にして似たようなものを作ることにした。RecyclerView
をなんとかして使えるようにすればよかったのかもしれないが、要件に合うLayoutManager
の作り方がわからなくてやめた。今思うとLayoutManager
は必要なかったかもしれない。- アニメーションについても
RecyclerView
のパターンを参考にしてItemAnimation
的なものでアニメーションをやったらいいのではないかと思い立ち、えいやっと追加した。この時にViewHolder
がいろんなインデックスを持っていることの意味をもう少し考えておくべきだった。 - ただ単にデータの更新によって
View
を付けたり外したりするだけならよかった。新しく増えたデータをView
にバインドした後で登場させるアニメーションや、データが減った時にView
を退場させるアニメーションのことを考慮しきれていなかった。データがなくなったらView
もなくなるというのが間違いだった。アニメーションが終わるまでremoveできない。その間にまたデータが復活するかもしれない。RecyclerView
の子View
は取り付けられてから外されるまで、インデックスやフラグの値を更新しながらView
のライフサイクルの中でLayoutされたりデータをバインドされたりアニメーションで動かされたりしながら使われなくなったらプールの中に入る。データがなくなった時必ずしも取り外されるとは限らない。 ViewPool
からアタッチされていないViewを探してくるところもうまくいってなかった。- アニメーションが終わったのをどうやって知るのがいいのかわからない。
- アニメーションは中断可能であり再入可能でなければならない。退場アニメーションの途中でデータが復活することがあるかもしれない。登場アニメーションの最中にデータが消えてしまうかもしれない。
lottie-androidの大雑把な処理の流れ
処理時間の性能改善をするとき、いつもはまずどこに伸びしろがありそうかあたりを付けるためにメソッドトレースのFlame Chartを見ている。LottieSample
の場合は大体setProgress()
かdraw
かそれ以外(プログレスバーとかログ出力)に分かれていて*1、これを見る限りだとsetProgress()
の方が若干多いように見えたので、手始めにsetProgress()
の処理時間を短縮する手立てを考えることにした。その過程で処理の流れがふんわりとわかるようになってきたので、今の段階で理解している内容を一旦まとめてみる。全体的な処理の流れは次の通り。
- まずはJSONをパースするなどして
LottieCompose
をつくる LottieCompose
をLottieAnimationView
やLottieDrawable
に渡すと、Layer
やKeyframe
,KeyframeAnimation
などのアニメーションに関係するオブジェクトが作られる- アニメーションが開始されたら基本的には以下の繰り返し
- setProgress
- draw
setProgress
レイヤーやグループといった単位に対するアニメーション可能な属性値、そしてそれらの子要素であるアニメーション部品の数だけsetProgressがよばれる。レイヤーのアニメーション可能な属性は今のところ9個ある。全部nullable。
- 透過度
- 開始の透過度
- 終了の透過度
- アンカーポイント
- 位置
- 倍率
- 回転
- 歪み
- 歪み角度
また、マスクやマット(つや消し)の効果があるときにもsetProgressが呼ばれる。一つのsetProgressはそこまで重くないのだけど、数がとにかく多い。
setProgessの中でいくつかのsetProgressが呼ばれその中でまたさらにいくつかのsetProgressが呼ばれ世はまさに大setProgress時代
— ありがとう日清カレーメシ (@akihito104) 2019年10月21日
このフェーズの中でDrawable.invalidateSelf()
がよばれると、drawフェーズに進むことになる。逆に言うと、誰もinvalidateしないということになればdrawフェーズには行かないのでここのチェックを正しく行うことは性能に大きく寄与する。また、描画に影響しないパーツや属性値はdrawフェーズでほぼ何もせず次に処理を回すことがあるので、全体的な見かけの消費時間がdrawフェーズより多くなることもある。仕方のない部分もあるが部分的にでもカットできればよさそう。
draw
それぞれのパーツがレイヤーごとに順番に描画される。ver. 3.1.0でオフスクリーンレンダリング(Canvas.saveLayer()
)に対応したので、透過度がAfter Effectsの見た目通りに効くようになった模様。性能面に影響があるので、オフスクリーンレンダリングはデフォルトではoff(従来の挙動)になっている。
このフェーズの処理を大雑把に言うと、移動や変形させるために値を集めて行列を作ったり、色、透過度の値を集めてきてまとめてパスに渡すという感じ。ここの性能はサンプルアプリでもいろんな形で見ることができる。そういう事情もあってか、チューニングがかなり進んでいる印象。行列がいつも同じ値なら予め計算しておいたものを使い続けるとか、パスに掛けておくみたいな事ができると短縮できるかもしれない。
実際にはView
なりDrawable
が表示される時にはonDraw()
が呼ばれるので、setProgress()
より先にdraw
が呼ばれることになる。ただ、アニメーションの最中はsetProgress()
で与えられた進捗の値を使って図形や文字の位置や大きさ、色を計算してdraw
でCanvas
に書くというのが基本的な流れになっているものと認識している。
*1:そういう風に分かれて見えるように修正した話はまた別の機会に
lottie-androidの色の補間処理の性能を改善するパッチが取り込まれた
これです
ChangelogにはMinor performance optimization
と書かれていることからもわかる通り全然大したことない改善なんだけど、今やっているプロジェクトではこういうのも馬鹿にはできないというレベルで厳しいので取り込んでもらえたのがありがたい。
どのような修正なのか
どんな修正かをざっくりいうと、GammaEvaluator.evaluate()
という色の補間を行うメソッドに対して開始にも終了にも同じ色が渡された時、補間の計算をせずに開始の色をすぐ返すようにした、というもの。
ある色を目的の色までなめらかに変化させたいとき、時間の進捗に応じた”間の色”を計算(補間)しなければならない。lottie-androidでは補間の際にガンマ補正を行っているぶん、人の見た目にはより自然にみえるようになるものの時間がかかる。
この時刻からこの時刻までの間にAをBまで変化させるということを、lottie-androidではKeyframe
というクラスで表現しており、動くと動かないとにかかわらず、Canvas(正確にはLayer)に書くものすべてがKeyframe
(あるいはそのサブクラス)で表現される。Keyframe
によっては色が変化しないというものもあるのだが、そのような場合にもlottie-androidでは色の補間計算を行うようになっていた。これは、ボトルネックを調べるためにAndroid StudioのプロファイラでJavaメソッドレコーディングをしたところ、色を変化させるアニメーションがないにもかかわらず件のメソッドが目に付くレベルで時間を消費していたことから発覚した。その部分のコードを読んでみて、大したインパクトはないかもしれないけど書き足す量もほんの少しだし、シンプルな変更なので特に異論なく取り込まれるのではないかと考えてプルリクを出した。
Keyframe
にはisStatic()
という真偽値を返すメソッドがあり、真の場合はそのKeyframe
は時刻にかかわらず常に同じ値を返すというものなのだが、色のKeyframe
の場合はそれが呼ばれず、色の補間を毎フレーム計算しているようだった。処理的にもっと手前の段階で計算をスキップするような処理を入れるべきなのかもしれなかったのだが、影響範囲が読めなくなってしまうので今回はやらなかった。
性能改善のための修正は宝探しをやっているようで、見つけるまではつらいが、探し当てるとすごく嬉しい。有名なOSSにも僕のような者が見つけられるような宝がまだ眠っているということが分かった。そうはいってもそれなりにチューニングされているので、もう本当にちょっとずつ削りだすような感覚でやっていかなければならない。