パフォーマンスとビュー階層

View オブジェクトの階層の管理方法が、アプリのパフォーマンスに重大な影響を及ぼすことがあります。このページでは、ビュー階層がアプリの速度を低下させているかどうかを評価する方法について説明し、発生する可能性がある問題に対処するための戦略をいくつか紹介します。

このページでは、View ベースのレイアウトの改善に焦点を当てます。Jetpack Compose のパフォーマンスを向上させる方法については、Jetpack Compose のパフォーマンスをご覧ください。

レイアウトと測定のパフォーマンス

レンダリング パイプラインには「レイアウトと測定」ステージが含まれており、システムがその間に、関連アイテムをビュー階層内に適切に配置します。このステージの測定パートでは、View オブジェクトのサイズと境界を決定します。一方、レイアウト パートでは、View オブジェクトを画面上のどこに配置するかを決定します。

どちらのパイプライン ステージでも、処理するビューまたはレイアウトごとにわずかなコストが発生します。ほとんどの場合、このコストは最小限で、パフォーマンスに大きな影響が及ぶことはありません。ただし、アプリが View オブジェクトを追加または削除すると(RecyclerView オブジェクトがリサイクルまたは再利用する場合など)、コストが高くなることがあります。また、View オブジェクトの制約に合わせてサイズ変更を検討する必要がある場合も、コストが高くなる可能性があります。たとえば、テキストを折り返す View オブジェクトでアプリが SetText() を呼び出す場合、View のサイズ変更が必要になることがあります。

このようなケースで時間がかかりすぎると、許容範囲の 16 ミリ秒以内にフレームをレンダリングできなくなり、それによりフレームがドロップされ、アニメーションの品質が低下することがあります。

これらの処理はワーカー スレッドに移動できず、アプリのメインスレッドで処理する必要があるため、最適化して、できる限り処理時間を短縮することをおすすめします。

複雑なレイアウトを管理する

Android レイアウトを使用すると、ビュー階層内で UI オブジェクトをネストできます。このネストでも、レイアウト コストがかかることがあります。アプリは、レイアウトのオブジェクトを処理するときに、レイアウトのすべての子でも同じ処理を行います。

一部の複雑なレイアウトでは、システムが初めてレイアウトを計算するときにのみコストが発生することがあります。たとえば、RecyclerView オブジェクトで複雑なリストアイテムをリサイクルする場合、すべてのオブジェクトをレイアウトする必要があります。別の例では、些細な変更が、親のサイズに影響しないオブジェクトに達するまで親に向かってチェーンを伝播することもあります。

レイアウトに時間がかかる理由としてよくあるのは、View オブジェクトの階層が互いにネストされていることです。ネストされた各レイアウト オブジェクトがレイアウト ステージにコストを追加します。階層がフラットであればあるほど、レイアウト ステージが完了するまでにかかる時間が短くなります。

RelativeLayoutLinearLayout ではなく、Layout Editor を使用して ConstraintLayout を作成することをおすすめします。これは、一般的に効率が高く、レイアウトのネストを減らすことができるためです。ただし、FrameLayout を使用して実現できるシンプルなレイアウトには、FrameLayout を使用することをおすすめします。

RelativeLayout クラスを使用している場合、ネストされているが重み付けされていない LinearLayout ビューを代わりに使用すると、コストを抑えながら同じ効果を獲得できることがあります。ただし、ネストされ、重み付けされた LinearLayout ビューを使用している場合は、次のセクションで説明するように、複数のレイアウトパスが必要になるため、レイアウトのコストが大幅に高くなります。

また、ListView ではなく RecyclerView を使用することをおすすめします。個々のリストアイテムのレイアウトをリサイクルできるため、効率性が向上し、スクロールのパフォーマンスも向上します。

二重負荷

フレームワークは通常、レイアウト ステージまたは測定ステージを単一のパスですばやく実行します。ただし、レイアウトが複雑なケースでは、階層部分に複数のパスを解決する必要がある部分があり、最終的に要素を配置する前に、その部分をフレームワークで繰り返し処理しなければならない場合があります。このレイアウトと測定を複数回繰り返す必要があることを「二重負荷」と呼びます。

たとえば、RelativeLayout コンテナを使用することで、他の View オブジェクトの位置に対して View オブジェクトを配置できます。この場合、フレームワークは次のシーケンスを実行します。

  1. レイアウトと測定のパスを実行します。フレームワークはその間に、各子のリクエストに基づいて各子オブジェクトの位置とサイズを計算します。
  2. このデータを使用し、オブジェクトの重みも考慮して、相互に関連付けられたビューの適切な位置を計算します。
  3. 2 回目のレイアウトパスを実行し、オブジェクトの最終的な位置を決定します。
  4. レンダリング プロセスの次のステージに進みます。

ビュー階層のレベルが高いほど、パフォーマンスが低下する可能性が高まります。

前述のように、ConstraintLayout は一般に、FrameLayout を除くほかのレイアウトよりも効率的です。これにより、複数のレイアウトパスが発生する可能性が低下し、多くの場合、レイアウトをネストする必要がなくなります。

RelativeLayout 以外のコンテナでも二重負荷が増加する可能性があります。次に例を示します。

  • LinearLayout ビューを横長にすると、レイアウトと測定のパスが二重に作成されることがあります。また、measureWithLargestChild を追加した場合にも、レイアウトと測定のパスが縦向きで二重に作成されることがあります。その場合、適切なサイズのオブジェクトを解決するために 2 つ目のパスをフレームワークで実行する必要があります。
  • GridLayout でも相対位置を指定できますが、通常は子ビュー間の位置関係を前処理することで、二重負荷を回避します。ただし、レイアウトでウェイトを使用するか Gravity クラスを設定すると、前処理のメリットが失われます。さらに、コンテナが RelativeLayout の場合、フレームワークで複数のパスを実行しなければならなくなることもあります。

レイアウトと測定のパスが複数あっても、必ずしもパフォーマンス上の負荷につながるとは限りません。ただし、場所が適切でないと負荷が発生するおそれがあります。コンテナが次のいずれかの条件に当てはまる場合は注意が必要です。

  • コンテナがビュー階層内のルート要素である。
  • コンテナの下に深いビュー階層がある。
  • ListView オブジェクトの子と同様に、コンテナのインスタンスが多数存在し、画面を占有している。

ビュー階層の問題を診断する

レイアウトのパフォーマンスは、さまざまなファセットを持つ複雑な問題です。次のツールを使用すると、パフォーマンスのボトルネックが発生している場所を特定できます。ツールによっては、明確な情報は得られませんが、有用なヒントを提供するものもあります。

perfetto

Perfetto は、パフォーマンスに関するデータを提供するツールです。これらの Android トレースは Perfetto UI で開くことができます。

GPU レンダリングのプロファイル作成

Android 6.0(API レベル 23)以降を搭載しているデバイスで使用できる、デバイス上の GPU レンダリングのプロファイル作成ツールでは、パフォーマンスのボトルネックに関する具体的な情報を確認できます。このツールを使用すると、レンダリングの各フレームについて、「レイアウトと測定」ステージにかかった時間を確認できます。このデータにより、実行時のパフォーマンスの問題を診断できます。また、レイアウトと測定の問題の内容と、その問題に対処する必要があるかどうかを確認できます。

GPU レンダリングのプロファイル作成では、キャプチャしたデータをグラフィックで表示する際に、青色でレイアウト時間を表します。このツールの使用方法について詳しくは、GPU レンダリングのプロファイル作成の速度をご覧ください。

lint

Android Studio の lint ツールを使用すると、ビュー階層の非効率性を認識できます。lint ツールを使用するには、図 1 に示すように、[Analyze] > [Inspect Code] の順に選択します。

図 1. Android Studio で [Inspect Code] を選択します。

各種レイアウト アイテムに関する情報は、[Android] > [Lint] > [Performance] に表示されます。各項目をクリックして展開すると、画面右側のペインに詳細情報が表示されます。図 2 に展開された情報の例を示します。

図 2. lint ツールで特定された問題に関する情報の表示

項目をクリックすると、その項目に関連する問題が右側のペインに表示されます。

このツールに関する具体的なトピックや問題について詳しくは、lint のドキュメントをご覧ください。

Layout Inspector

Android Studio の Layout Inspector ツールには、アプリのビュー階層が視覚的に表示されます。アプリの階層を操作するのに役立ち、特定のビューの親チェーンをわかりやすく視覚的に表示できるほか、アプリが作成したレイアウトを詳細に調べられます。

Layout Inspector が表示するビューは、二重負荷に起因するパフォーマンスの問題を特定するのにも役立ちます。また、パフォーマンス コストの原因となり得る、ネストされたレイアウトの深いチェーンや、ネストされた子が大量にあるレイアウト領域を特定する手段にもなります。これらのケースでは、レイアウトと測定の段階でコストがかかり、パフォーマンスの問題が発生する可能性があります。

詳しくは、Layout Inspector と Layout Validation を使用してレイアウトをデバッグするをご覧ください。

ビュー階層の問題を解決する

ビュー階層に起因するパフォーマンスの問題を解決する基本的なコンセプトは、実際には難しい場合があります。ビュー階層によるパフォーマンスの低下を防ぐには、ビュー階層をフラット化し、二重負荷を減らす必要があります。このセクションでは、これらの目標を達成するための戦略について説明します。

ネストされた冗長なレイアウトを削除する

ConstraintLayout は、レイアウト内にビューを配置するためのさまざまなメカニズムを備えた Jetpack ライブラリです。これにより、1 つの ConstaintLayout をネストする必要がなくなり、ビュー階層をフラット化できます。通常は、他のレイアウト タイプと比べて、ConstraintLayout を使用して階層をフラット化する方が簡単です。

多くの場合デベロッパーは、必要以上にネストされたレイアウトを使用します。たとえば、RelativeLayout コンテナには、単一の子(これも RelativeLayout コンテナ)が含まれることがあります。このネストは冗長であり、ビュー階層に不要なコストが発生します。多くの場合、lint からこうした問題が報告されるため、デバッグ時間を短縮できます。

merge または include を採用する

ネストされた冗長なレイアウトが作成される主な原因は、<include> タグです。たとえば、再利用可能なレイアウトを次のように定義することがあります。

<LinearLayout>
    <!-- some stuff here -->
</LinearLayout>

次に、<include> タグを追加して、親コンテナに次の項目を追加します。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

後に続く include によって 1 つ目のレイアウトが 2 つ目のレイアウト内に不必要にネストされています。

<merge> タグを使用すると、このような問題を予防できます。このタグの詳細については、<merge> タグを使用するをご覧ください。

低コストのレイアウトを採用する

冗長なレイアウトを含まないように、既存のレイアウト スキームを調整することができない場合もあります。場合によっては、まったく別のレイアウト タイプに切り替えて階層をフラット化することが唯一のソリューションであることもあります。

たとえば、TableLayout は、位置的な依存関係を多く含む複雑なレイアウトと同じ機能を提供します。Jetpack ライブラリ ConstraintLayout には、RelativeLayout と同様の機能に加えて、よりフラットで効率的なレイアウトの作成に役立つ機能が用意されています。