前回の記事作成から、また1年以上が過ぎてしまいました。
もうちょっと頑張りたい気もしますが、たぶん今後もこのままゆるゆるなペースで書いて行くと思います。
さて、前回は若干ニッチな内容だったのを反省し、今回はもっと幅広い層に有用そうな高速化ネタにしてみました。
高速化と言いつつ計測がメインで、こんなバカな書き方しちゃダメだよ、という話が主な内容です。
※バカな書き方をしてしまった経験談は記事の末尾を参照
Androidのレイアウト生成速度に関わる基本的なお話
Androidのレイアウト生成処理は一般的に以下のような流れかと思います。
- layout resource ファイルの読み込み(setContentView や inflate など)
- View の生成(1 に含まれる気もしますが、記事の都合上分けて書いてます)
- View のサイズ計算
- View のレイアウト
そしてパフォーマンスについて勉強を始めると、基本的な点として以下のような内容をよく見かけると思います。
- ListView のスクロール時に毎回Viewの再生成が走らないように ViewHolder を使おう
- View をネストしまくるとレイアウトが重くなるので LinearLayout を RelativeLayout に置き換えるなど、ネストを浅くできないか検討しよう
前者が前述の 1~2 の部分、後者が 3~4 の部分に当たるかと思います。
そしてこの記事は 2 のView の生成
についての話となります。
なぜここに絞ったかと言うと、一般的な開発において対処がしやすく、かつ、他の箇所よりも改善時の効果が圧倒的に大きいからです。
1 は改善しづらいし、3~4 は相当ひどい実装をしていない限りは最近の端末だと改善しても大した効果が見込めません。
そもそも View のnew の速度
が圧倒的に遅い場合があるのです。
View生成速度計測
というわけで、標準(サポートライブラリ含む)で用意されている一般的な 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生成速度傾向
最上位クラスである Viewクラス
が一番軽いのは当然として、ぱっと見以下のような傾向があります。
- Button とそのサブクラスが軒並み遅い(ImageButton は ImageView のサブクラス)
- XXXLayout 系のクラスはそんなに遅くない
レイアウト生成速度の改善
今までの話や測定結果を見た上で、じゃあ速度を改善するためにはどうすれば良いかという話になりますが、
非常に当たり前のことばかりなのですが以下の2点が重要になります。
なるべく生成コストの低いViewを使用する
「機能を一切使ってないにも関わらず無駄に高機能なViewを使用している」
案外これ、やっちゃってることあると思います。
例えば、やりたいことが以下のような場合について考えてみます。
- OSバージョンに関係なく、なるべく同じ見た目のボタンを設置したい
- タップ時のエフェクトは欲しい
- タップ時に何かしら処理を行いたい
要件がこれだけであれば最上位クラスである View
を使い、以下の実装をするだけで事足りる可能性があります。
- background に state_pressed の状態によって見た目が変わる drawable を設定する
- OnClickListener を実装&設定する
上記に加え、ボタンに任意の文字列を表示したい場合は TextView
を、設定した画像に対して各種設定を行いたい場合は ImageView
などを使うことになるかと思います。
特に初心者がよくやってしまいそうなのが以下のような実装です。
- 特徴のある style を備えたクラスを使用しているにも関わらず、標準の style を全く使用せず任意の画像(または透明)を表示するよう変更している
- 該当クラスに存在する、自作が面倒な機能を使いたい場合などは仕方がない
- ボタンという言葉に惑わされて意味もなく
Button
クラスを使用しているケースが多そう
ちなみに、 Button
クラスは実装を見れば分かりますが、TextView を継承して専用の style を読み込んでいる以外ほとんど何もしていません。
つまり、この style を使うつもりがないならまず使う必要がありません。
そもそも生成コスト自体が高いので、生成する View の数を減らす
例えば何かしらの進捗率を上記のような見た目で表示したいとします。
これを進捗を表す〇の数を可変にできるような形で既存のViewで実現しようとすると、drawable を設定したViewを〇の数だけ生成し、進捗が変わるたびに drawable を直接差し替えるか state_checked などを利用して切り替わるようにする必要があるかと思います。
上の画像の例だと〇が30個表示されているのでViewを30個生成することになり、もしこれを ListView の各項目ごとに表示しようものなら簡単に数百個を超えてしまいます。
これを、以下のような機能を備えた自作のViewに差し替えれば1つのViewで済みます。
- 〇の数を設定できる
- 進捗を表す〇の見た目(2種)を設定できる
- 進捗率を設定できる
- 上記設定に応じて draw を行う
特に View の draw メソッドを実装したことがない人の場合、こういう自作のViewを作るのに若干のハードルを感じるかも知れません。
しかし、サイズ計算を固定値にするなど実装を妥協すれば前述したような「可変数のViewの生成」「進捗率に応じて全Viewの設定し直し」より実装量が少なくなる可能性もあり、View を使用する側(Fragmentなど)のコードもスッキリすると思うので1度チャレンジしてみる価値はあると思います。
※自作Viewの作成に関しての詳細は検索すれば色々出てくると思うので割愛
バカな書き方をしてしまった経験談
以上で本記事における計測調査、改善についての手法説明については終わりたいと思います。
最後に、自分への戒めの意味も込めてバカな書き方をしてしまった経験談を載せておきます。
やってしまったこと
- 1つの状態を表す機能(レイアウト)を大量のViewを生成・組み合わせる形で実装していた
- 「そもそも生成コスト自体が高いので、生成する View の数を減らす」記載の進捗率の例とほぼ同じ内容
- 大量生成している View は
state_checked
を使いたいという理由だけでToggleButton
を使用していた
誰だこんなクソ実装書いたやつって感じですね。(自分です)
上記の実装を含むレイアウトを ViewPager
内に存在する ListViewの各項目ごと
に表示していたので本当にひどかったです。
ListView で ViewHolder を使った実装をしようが、初期表示時にはなんの効果もないので、
ViewPager を横スクロールして新しい画面が生成されるたびに 数百ms 発生してカクっと操作が止まってしまっていました。
その時の改善方法
もちろん一番いいのは draw
まで行う自作の単一Viewに差し替えることです。
が、この時は処理の差し替えだけにそんなに工数をかけられなかったため ToggleButtonの生成が鈍い
点に着目し、ToggleButton を Checkable を実装した自作View
へ差し替えるという対応をしました。
実装方法はこの辺りの記事が参考になるかと思います。
この時は Checkable を 実装した ImageView へ差し替えたのですが、計測結果の通り ToggleButton
と ImageView
には生成速度の差が 10倍以上 あるため、単一Viewへの差し替えと比べると完璧とは言えませんがそれでも体感速度は大幅に向上しました。