エキサイト株式会社メディア事業部エンジニアの武藤です。
Android の ConstraintLayout を使ってレイアウトを実装する際に、レンダリングパフォーマンスに問題があるケースがあったので紹介します。
問題が起きたケース
開発が進んでレイアウトが複雑になってきたときに、リストページのスクロールにカクつきが出てきました。
その際のレイアウトの簡略版が下記です。
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <FrameLayout> <androidx.core.widget.NestedScrollView> <androidx.constraintlayout.widget.ConstraintLayout> <RecyclerView /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> <androidx.constraintlayout.widget.ConstraintLayout> <ProgressBar /> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.constraintlayout.widget.ConstraintLayout> <TextView android:id="@+id/text_error" ... /> <Button android:id="@+id/button_reload" ... /> </androidx.constraintlayout.widget.ConstraintLayout> </FrameLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
RecyclerView を使ったリストに、 API との通信中に表示する ProgressBar、通信エラー時にメッセージを出す TextView, 再読み込みをする Button を別々の ConstraintLayout の中で配置していました。
スペックの低い端末でわずかにカクついており、実装中には気づきにくいものでした。
改善したレイアウト
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <androidx.constraintlayout.widget.ConstraintLayout> <RecyclerView /> <ProgressBar /> <androidx.constraintlayout.widget.Group app:constraint_referenced_ids="text_error, button_reload" /> <TextView android:id="@+id/text_error" ... /> <Button android:id="@+id/button_reload" ... /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
通信エラー時に表示する TextView, Button は、constraintlayout.widget.Group によってグループ化し、表示・非表示を切り替えるように変更しました。
最終的に ConstraintLayout を1つにまとめたところで改善に至りました。結果から原因の推察になりますが、ConstraintLayout を複数並べることがパフォーマンスに影響を及ぼしていたようです。
この件を調べてみると、いくつか同様の記事が見つかりました。 (もっと早く出会いたかった。。)
既存アプリのレイアウトをConstraintLayoutに書き換えた - ビー鉄のブログ
ConstraintLayoutをネストすると激重になる - Qiita
ConstraintLayout を扱う際は、入れ子や複数の羅列は避けてレイアウトを組むように注意すべきです。
なぜ気づけなかったのか?
実装時に気づけなかったことについて振り返ってみました。
まず起きているカクつき自体が、エミュレータや手元の実機ではわかりにくく環境依存の点があったと思います。実際に発覚したのはstageビルドで他の検証機を使った段階でしたので、どこの修正で影響を及ぼしたのかが追いにくい状況になっていました。
また、経験的にも多少のネストのネストでそこまでパフォーマンスに影響があるケースがなかったので、すぐにレイアウトの問題という発想には至りませんでした。一般的にパフォーマンス低下の要因には、レイアウト以外にも考えられ、複数の通信をする場合に同期的な実装になっていることやデータの処理側の可能性もあり、検討をつけにくくなっていました。
パフォーマンスへの影響には様々な要因が考えられ、問題の解決に時間がかかってしまいました。
レンダリングパフォーマンスを意識した実装
Android のレンダリングパフォーマンスを考えるにあたって、View の階層化は気をつけなければいけない問題の一つです。
これはAndroid の公式ドキュメントでも言及されており、開発者は注意しながらレイアウトの構築をする必要があります。
レイアウトに特に時間がかかる最も一般的なケースは、View オブジェクトの階層が互いにネストされている場合です。ネストされた各レイアウト オブジェクトがレイアウト ステージにコストを追加します。階層がフラットであればあるほど、レイアウト ステージが完了するまでにかかる時間が短くなります。
公式ドキュメントではレイアウトのネストについて上記のように説明されており、ConstraintLayout に限らず、ネストを避けたレイアウトを組むことが重要です。
ConstraintLayout はネストを回避したレイアウト構築が可能なコンポーネントであり、View に制約を付与することで位置関係を定義します。
今回のケースでは ConstraintLayout を使いつつも、リストとエラー表示系を別々のレイアウトに配置していたことが原因でした。実際には単一の ConstraintLayout で実現でき、誤った使い方が招いた問題でした。
GPUプロファイルを見てみる
レンダリングのパフォーマンスに問題があるときに、原因の究明を助ける方法が公式ドキュメントで紹介されています。
今回はGPUプロファイルを使うことで、ヒントを得られました。
設定方法を紹介します。
Android 端末の開発向けオプションを開いて 「HWUI レンダリングのプロファイル作成」から 「バーとして画面に表示」をクリックします。
画面上に色付きのグラフが表示されます。 ここでは、改善前のサンプルアプリでGPUプロファイルを見てみます。
色の意味は公式ドキュメントで説明されており、それを足がかりに原因を考えてみました。
図では右側の黄緑と深緑部分が長くなっており、処理に時間を要していることが分かります。
黄緑は「Measurement / Layout(測定 / レイアウト)」を意味しており、ここが長い場合は、レイアウトに原因があることが考えられます。今回のケースでは、これをきっかけにレイアウトの見直しができました。
深緑は「その他」を意味しており、メインスレッドで重い処理が実行されている可能性が考えられます。こちらもリスト面のデータ通信が悪いのかなどの検討に繋がりました。
その他のパフォーマンス調査の方法
公式ドキュメントでは、GPUプロファイル以外にもパフォーマンスの調査方法が紹介されていますが、いくつか試してみてGPUプロファイルが手軽に試せたのが良かったです。
その他の方法についての所感です。
- Systrace
- 詳細な情報が確認できる反面、その情報がどういうものなのかを調べる必要があり、扱うのが難しく断念しました。。
- lint
- 冗長な実装がある場合にワーニングを表示してくれます。今回のケースではヒントとなる情報は得られませんでしたが、通常の開発時に一度使って無駄がないかを調べるのに有用と感じました。
- Layout Inspector
まとめ
Android の ConstraintLayout を扱う際の注意点とレンダリングパフォーマンス問題の解決の緒について説明しました。 Layout のネストの注意点は基本的ではありますが、私の開発経験では意識しなくとも問題になるケースがなかったので、解決にたどり着くまでに時間を要しました。 プロダクト開発では段階によって問題が複雑化し、問題の切り分けが難しくなってきます。 そういった問題に対して、GPU プロファイルを確認して原因を調査していくことで、改善につなげられました。