Androidのレイアウト生成速度を改善する

前回の記事作成から、また1年以上が過ぎてしまいました。
もうちょっと頑張りたい気もしますが、たぶん今後もこのままゆるゆるなペースで書いて行くと思います。

さて、前回は若干ニッチな内容だったのを反省し、今回はもっと幅広い層に有用そうな高速化ネタにしてみました。
高速化と言いつつ計測がメインで、こんなバカな書き方しちゃダメだよ、という話が主な内容です。
※バカな書き方をしてしまった経験談は記事の末尾を参照

Androidのレイアウト生成速度に関わる基本的なお話

Androidのレイアウト生成処理は一般的に以下のような流れかと思います。

  1. layout resource ファイルの読み込み(setContentView や inflate など)
  2. View の生成(1 に含まれる気もしますが、記事の都合上分けて書いてます)
  3. View のサイズ計算
  4. 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 へ差し替えたのですが、計測結果の通り ToggleButtonImageView には生成速度の差が 10倍以上 あるため、単一Viewへの差し替えと比べると完璧とは言えませんがそれでも体感速度は大幅に向上しました。

関連記事

AndroidのTextViewへHTMLマークアップによるスタイル設定(文字列装飾)を行う

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ