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

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

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

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

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

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

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

複雑さの管理: レイアウトが重要

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

レイアウトに特に時間がかかる最も一般的なケースは、View オブジェクトの階層が互いにネストされている場合です。ネストされた各レイアウト オブジェクトがレイアウト ステージにコストを追加します。階層がフラットであればあるほど、レイアウト ステージが完了するまでにかかる時間が短くなります。

RelativeLayout クラスを使用している場合、ネストされているが重み付けされていない LinearLayout ビューを代わりに使用すると、コストを抑えながら同じ効果を獲得できることがあります。また、アプリの対象が Android 7.0(API レベル 24)の場合、特別なレイアウト エディタを使用すると、RelativeLayout オブジェクトの代わりに ConstraintLayout オブジェクトを作成できる高い可能性があります。こうすることで、このセクションで説明する問題の多くを回避できます。ConstraintLayout クラスは、同様のレイアウト制御を提供するだけでなく、パフォーマンスを大幅に向上させます。このクラスでは独自の制約解決システムを使用して、標準のレイアウトとは大きく異なる方法でビュー間の関係を解決します。

二重課税

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

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

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

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

RelativeLayout 以外のコンテナでも二重課税が発生することがあります。次に例を示します。

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

レイアウトと測定の複数のパスは、それ自体ではパフォーマンスの負担になりません。しかし、パスの場所が間違っていると、パフォーマンスの負担になることがあります。コンテナが次のいずれかの条件に当てはまる場合は注意が必要です。

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

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

レイアウトのパフォーマンスは、さまざまなファセットを持つ複雑な問題です。パフォーマンス ボトルネックの発生場所を確実につきとめられるツールがいくつかあります。他にも、情報の確実性は低いものの有用なヒントが得られるツールがいくつかあります。

Systrace

Android SDK には、パフォーマンスに関する優れたデータを提供するツールである Systrace が組み込まれています。Systrace ツールを使用すると、Android デバイス上で実行中のすべてのプロセスのタイミング情報を収集、検査できます。これにより、レイアウトのパフォーマンスの問題がパフォーマンスの問題を引き起こすタイミングを特定できます。Systrace について詳しくは、システム トレースの概要をご覧ください。

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

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

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 Inspector でレイアウトをデバッグするをご覧ください。

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

ビュー階層に起因するパフォーマンスの問題を解決する際の基本的なコンセプトはシンプルなものですが、実際の作業は困難を伴います。ビュー階層に起因するパフォーマンスの低下を防止するには、ビュー階層のフラット化と二重課税の緩和という 2 つの目標の達成が必要です。このセクションでは、これらの目標を達成するための戦略について説明します。

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

多くの場合デベロッパーは、必要以上にネストされたレイアウトを使用します。たとえば、RelativeLayout コンテナには、単一の子(これも RelativeLayout コンテナ)が含まれることがあります。このネストは冗長であり、不必要なコストがビュー階層に追加されます。

多くの場合、lint からこうした問題が報告されるため、デバッグ時間を短縮できます。

merge / include を採用する

ネストされた冗長なレイアウトが作成される主な原因の 1 つは、<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 タグについて詳しくは、<include> でレイアウトを再利用するをご覧ください。

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

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

たとえば TableLayout には、位置に関する依存関係を多数持つ複雑なレイアウトと同じ機能があります。N リリースの Android では、ConstraintLayout クラスにより RelativeLayout が同様の機能を得ており、コストは大幅に低減されています。