- Ruby / Rails以外の開発一般
READ MORE
前回の記事作成から、また1年以上が過ぎてしまいました。
もうちょっと頑張りたい気もしますが、たぶん今後もこのままゆるゆるなペースで書いて行くと思います。
さて、前回は若干ニッチな内容だったのを反省し、今回はもっと幅広い層に有用そうな高速化ネタにしてみました。
高速化と言いつつ計測がメインで、こんなバカな書き方しちゃダメだよ、という話が主な内容です。
※バカな書き方をしてしまった経験談は記事の末尾を参照
Androidのレイアウト生成処理は一般的に以下のような流れかと思います。
そしてパフォーマンスについて勉強を始めると、基本的な点として以下のような内容をよく見かけると思います。
前者が前述の 1~2 の部分、後者が 3~4 の部分に当たるかと思います。
そしてこの記事は 2 のView の生成
についての話となります。
なぜここに絞ったかと言うと、一般的な開発において対処がしやすく、かつ、他の箇所よりも改善時の効果が圧倒的に大きいからです。
1 は改善しづらいし、3~4 は相当ひどい実装をしていない限りは最近の端末だと改善しても大した効果が見込めません。
そもそも View のnew の速度
が圧倒的に遅い場合があるのです。
というわけで、標準(サポートライブラリ含む)で用意されている一般的な View の生成速度を計測してみました。
private static long execute(Runnable runnable) {
long base = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
runnable.run();
}
return System.currentTimeMillis() - base;
}
計測コードは上記のような感じで execute(() -> new View(this))
とかで呼び出してます。
また、以下に記載する計測値は上記メソッドを各10回実施し後半の5回分の平均値を使用しています。(前半の2~3回について誤差が大きい端末があったため)
ループ回数は少なすぎると 1ms 未満になるケースがあったため、以下の値は全てnew 500回にかかった時間
となります。
クラス名 | SOV31 (5.0.2) |
SCL24 (6.0.1) |
Lavie Tab E (7.1.1) |
PRA-LX2 (8.0.0) |
Pixel3XL (9.0) |
---|---|---|---|---|---|
View | 6ms | 52ms | 9ms | 3ms | 1ms |
ImageView | 15ms | 57ms | 18ms | 5ms | 1ms |
ImageButton | 54ms | 200ms | 76ms | 63ms | 21ms |
TextView | 93ms | 425ms | 95ms | 138ms | 40ms |
EditText | 231ms | 1063ms | 229ms | 340ms | 156ms |
Button | 340ms | 746ms | 228ms | 288ms | 76ms |
ToggleButton | 451ms | 944ms | 287ms | 359ms | 92ms |
RadioButton | 188ms | 621ms | 189ms | 275ms | 69ms |
CheckBox | 154ms | 679ms | 194ms | 279ms | 70ms |
Switch | 329ms | 1167ms | 235ms | 355ms | 90ms |
ProgressBar | 111ms | 247ms | 139ms | 113ms | 46ms |
SeekBar | 420ms | 1321ms | 368ms | 311ms | 86ms |
Spinner | 211ms | 1009ms | 313ms | 235ms | 183ms |
ListView | 181ms | 1336ms | 160ms | 172ms | 49ms |
GridView | 168ms | 1232ms | 173ms | 133ms | 44ms |
RecyclerView | 100ms | 361ms | 153ms | 157ms | 14ms |
LinearLayout | 30ms | 115ms | 33ms | 22ms | 11ms |
RelativeLayout | 33ms | 124ms | 43ms | 24ms | 13ms |
FrameLayout | 21ms | 106ms | 30ms | 20ms | 10ms |
GridLayout | 32ms | 154ms | 44ms | 26ms | 15ms |
TableRow | 24ms | 117ms | 37ms | 22ms | 11ms |
ConstraintLayout | 82ms | 280ms | 49ms | 34ms | 16ms |
※RecyclerView は androidx 1.0.0、ConstraintLayout は androidx 1.1.3
実際に各Viewを利用する際は各種設定値の読み込みなどでもう少し生成速度が遅くなると思いますが、new だけでもこれだけ時間がかかります。
端末によっても大分速度差が出ていますが、View の種類によっては1つ生成するのに 1ms 以上かかっているものもあります。
これを見て「なんだ大したことないじゃん」と思う方もいらっしゃるかも知れませんが、例えば1画面で100個生成すると 100ms 以上つまり 0.1秒以上かかってしまいます。
高速なスクロール処理などではUIスレッド上の1回の処理時間(draw を含む)の理想は 60fps を維持できる 16ms 以下ですが、画面生成時であればもう少し時間がかかってもほとんど体感には影響ないかと思います。
しかし、ViewPager などに表示する画面の生成時に 100ms 以上かかってしまうと横スクロール時にカクつきが目立ち始め、 200ms にもなるとカクつきが非常に顕著になります。
最上位クラスである Viewクラス
が一番軽いのは当然として、ぱっと見以下のような傾向があります。
今までの話や測定結果を見た上で、じゃあ速度を改善するためにはどうすれば良いかという話になりますが、
非常に当たり前のことばかりなのですが以下の2点が重要になります。
「機能を一切使ってないにも関わらず無駄に高機能なViewを使用している」
案外これ、やっちゃってることあると思います。
例えば、やりたいことが以下のような場合について考えてみます。
要件がこれだけであれば最上位クラスである View
を使い、以下の実装をするだけで事足りる可能性があります。
上記に加え、ボタンに任意の文字列を表示したい場合は TextView
を、設定した画像に対して各種設定を行いたい場合は ImageView
などを使うことになるかと思います。
特に初心者がよくやってしまいそうなのが以下のような実装です。
Button
クラスを使用しているケースが多そうちなみに、 Button
クラスは実装を見れば分かりますが、TextView を継承して専用の style を読み込んでいる以外ほとんど何もしていません。
つまり、この style を使うつもりがないならまず使う必要がありません。
例えば何かしらの進捗率を上記のような見た目で表示したいとします。
これを進捗を表す〇の数を可変にできるような形で既存のViewで実現しようとすると、drawable を設定したViewを〇の数だけ生成し、進捗が変わるたびに drawable を直接差し替えるか state_checked などを利用して切り替わるようにする必要があるかと思います。
上の画像の例だと〇が30個表示されているのでViewを30個生成することになり、もしこれを ListView の各項目ごとに表示しようものなら簡単に数百個を超えてしまいます。
これを、以下のような機能を備えた自作のViewに差し替えれば1つのViewで済みます。
特に View の draw メソッドを実装したことがない人の場合、こういう自作のViewを作るのに若干のハードルを感じるかも知れません。
しかし、サイズ計算を固定値にするなど実装を妥協すれば前述したような「可変数のViewの生成」「進捗率に応じて全Viewの設定し直し」より実装量が少なくなる可能性もあり、View を使用する側(Fragmentなど)のコードもスッキリすると思うので1度チャレンジしてみる価値はあると思います。
※自作Viewの作成に関しての詳細は検索すれば色々出てくると思うので割愛
以上で本記事における計測調査、改善についての手法説明については終わりたいと思います。
最後に、自分への戒めの意味も込めてバカな書き方をしてしまった経験談を載せておきます。
state_checked
を使いたいという理由だけで ToggleButton
を使用していた誰だこんなクソ実装書いたやつって感じですね。(自分です)
上記の実装を含むレイアウトを ViewPager
内に存在する ListViewの各項目ごと
に表示していたので本当にひどかったです。
ListView で ViewHolder を使った実装をしようが、初期表示時にはなんの効果もないので、
ViewPager を横スクロールして新しい画面が生成されるたびに 数百ms 発生してカクっと操作が止まってしまっていました。
もちろん一番いいのは draw
まで行う自作の単一Viewに差し替えることです。
が、この時は処理の差し替えだけにそんなに工数をかけられなかったため ToggleButtonの生成が鈍い
点に着目し、ToggleButton を Checkable を実装した自作View
へ差し替えるという対応をしました。
実装方法はこの辺りの記事が参考になるかと思います。
この時は Checkable を 実装した ImageView へ差し替えたのですが、計測結果の通り ToggleButton
と ImageView
には生成速度の差が 10倍以上 あるため、単一Viewへの差し替えと比べると完璧とは言えませんがそれでも体感速度は大幅に向上しました。