PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Androidアプリを軽くするレイアウトの3つのコツ

こんにちは。OTTサービス技術部 開発第3グループの古川です。

みなさんレイアウトの書き方を気にしてますか? AndroidやAndroidTVはさまざまなスペックの端末がありそれぞれでなるべく変わらない操作感を提供する必要があります。 そのために描画のパフォーマンスはとても大事な要素なのですが今回はレイアウトの書き方の観点からパフォーマンスのちょっとした工夫で軽くなるコツをまとめてみました。

この記事で解説すること

今回はレイアウトの中でもレイアウトXMLに焦点をあてて具体例も交えながらご紹介いたします。

  • ネスト(View階層)の深さを避ける方法
  • レイアウトパラメータの指定方法
  • 動的生成の回避とViewの再利用テクニック

ネスト(View階層)の深さを避ける方法

自分が今回この記事を書くきっかけにもなったのがこのネストです。 AndroidTVの実装中、どうしても描画が遅くなる箇所がありました。 API通信や取得した情報のサイズなど、さまざまな要因を調査しましたが、レイアウトの不要なネストを一つ解消するだけで、格段に処理が軽くなることを経験しました。 これをきっかけに、レイアウトの書き方を改めようと思った次第です。

深い階層が生む問題

レイアウトXMLではViewやViewGroupをツリー状(階層構造)で定義します。 この階層が「深い=ネストが多い」ことで、主に次のような問題が発生します。

メモリ消費の増加

ViewやViewGroupが増えるほど、アプリで使われるメモリ量も増加。 メモリ不足によるアプリクラッシュや、GC(ガベージコレクション)頻度増加でさらなる遅延に繋がる恐れも。

レンダリング速度の低下&画面の遅延

遅い端末(低スペックなスマホやAndroid TV)では、画面の表示自体が遅れ“カクつき”が起きたりタップやスクロールなどの、ユーザーの操作反応ももたつきやすくなってしまいます。

保守・可読性の低下

これは直接的にパフォーマンスにつながるわけではないですが可読性が下がりメンテナンスがしづらくなってしまいます。

ネストの浅い構造を実現するテクニック

ではどうやってネストを少しでも浅く、フラットにレイアウトを構築できるのかいくつか紹介します。

ConstraintLayoutの活用

ConstraintLayoutは複数のViewを、親子関係ではなく“制約”でつなぐレイアウトです。 従来のLinearLayoutやRelativeLayoutだと、ちょっと凝ったUIであればどうしてもネスト数が増えがちですが、ConstraintLayoutなら一枚で複雑な配置を実現可能です。

以下は単純なテキスト+ボタンの並びを表示するレイアウトですが従来のLinearLayoutやRelativeLayoutを使用した場合このように複数のViewGroupで過剰ラップしてしまい描画やレイアウト計算の負荷がすごく高くなってしまいます。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="タイトル"/>

                <Button
                    android:id="@+id/button"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="ボタン"/>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>

ConstraintLayoutを用いて同じものを表示するレイアウトを作成すると以下のように少ない階層で実装できます。

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/button"
        app:layout_constraintTop_toTopOf="parent"/>
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

includeタグで再利用&ネスト削減

includeタグを使用し同じレイアウト構造を複数箇所で使い回すことで無駄なネストが抑えられ、また可読性やメンテナンス性も向上します。

以下はincludeタグを用いずにレイアウトを書いた場合

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!-- 1回目 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/title_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="タイトル1" />

        <TextView
            android:id="@+id/subtitle_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="サブタイトル1" />
    </LinearLayout>

    <!-- 2回目(コピペ) -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/title_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="タイトル2" />

        <TextView
            android:id="@+id/subtitle_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="サブタイトル2" />
    </LinearLayout>
</LinearLayout>

これをincludeタグを用いることで以下のように書き換えることができます。 再利用したいレイアウトを別ファイルで作成し使用したいレイアウトでincludeタグを用いて再利用します。

<!-- res/layout/title_with_subtitle.xml:再利用したい共通のレイアウト -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="タイトル" />
    <TextView
        android:id="@+id/subtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="サブタイトル" />
</LinearLayout>
<!-- activity_main.xmlなどでincludeを利用して再利用 -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!-- 1回目 -->
    <include layout="@layout/title_with_subtitle"/>

    <!-- 2回目 -->
    <include layout="@layout/title_with_subtitle"/>

</LinearLayout>

無駄なViewの排除

paddingやmarginなどの空白を指定するだけのためにViewGroupを作成するのもネストが深くなる原因になります。 前述した通りConstraintLayoutで定義したりGuidelineを使う方がフラットで軽いです。

レイアウトパラメータの指定方法

AndroidのレイアウトXMLでよく使うlayout_widthやlayout_heightの値(wrap_content、match_parent、固定dp)にはそれぞれ特性とパフォーマンス面での違いがあります。 それぞれの特性を理解し状況に適したパラメータを使用することで計算の負荷を抑えることが可能です。

wrap_content, match_parent, 固定dpの違い

wrap_content

子Viewやテキストなど「中身に合わせて」サイズを自動計算してくれる

メリット: 内容にあわせてぴったりサイズになり、個別調整が不要

デメリット: 「実際の中身のサイズを毎回計算」する必要があるため、計算コストが高い。多用すると親→子→孫…と再帰的に何度も計算され、特に複雑なネストやリスト内要素ではパフォーマンス悪化しやすい

match_parent

親Viewのサイズに幅・高さを合わせる。「親いっぱい」になる

メリット: 親のサイズ参照だけなので計算が単純。多くのケースでパフォーマンス良好

デメリット: 必ずしも見た目が狙い通りにはならない場合がある(親が大きすぎる場合など)

固定dp(例:100dpなど)

ぴったり決まった幅や高さを指定する

メリット: 計算不要。描画・measure工程がシンプルかつ高速。パフォーマンス的には最も軽い

デメリット: デザイン上の柔軟性(文字量が増えた時など)はなく、全体設計への配慮が必要

計算負荷の観点から見たベストプラクティス

不要なwrap_content多用を避ける
特にリスト内(RecyclerViewのitem/layoutや複雑なネスト内)でのwrap_contentは負荷が高いので、できる限りmatch_parentや固定dpを優先する。 複数階層のViewGroupでwrap_contentが連鎖すると、「子要素→親要素→また子要素…」とmatch_parentや固定dpが指定されている箇所まで無限的にmeasureが走ってしまうので避ける。

textや画像Viewに固定dpを活用
内容がほぼ決まっている場合はパフォーマンスに優れた固定dpを用いる。

画面全体やリスト系ではmatch_parentが基本
画面幅いっぱい、画面高いっぱいレイアウトならmatch_parent推奨。

動的生成の回避とViewの再利用

こちらはレイアウトXMLだけで解決するものではないのですがAndroidのUIパフォーマンスを高めるには、「Viewの動的生成をなるべく避ける」「Viewを上手に再利用する」がとても重要です。
とくにリスト表示や大きな画面を持つAndroidTVアプリでは欠かせないテクニックです。

なぜ動的生成は重くなりやすいのか

  • ViewやViewGroupをプログラムで動的に大量生成すると毎回findViewByIdやnewでメモリ確保・measure/layout/描画処理が走り、初期化コストとGC(ガベージコレクション)発生が増大。
  • ユーザー操作やスクロールごとに、都度Viewインスタンスを新規作成するとアプリ全体がもっさりしがち。

RecyclerView/ViewHolder運用のベストプラクティス

今回は一例としてRecyclerViewとViewHolderについて簡単にご紹介します。

RecyclerViewとViewHolderパターンはViewの再利用のために生まれた設計です。 こちらを運用することで過度なViewの再生成を行わず動的なレイアウトの実現が可能です。

RecyclerView

RecyclerViewは「今画面に見えている範囲」だけ必要最小限Viewを保持し、 スクロールで表示外になったViewを再利用して新しいデータだけバインドする。

ViewHolder

1つのViewの中でfindViewByIdの回数を減らすための「キャッシュ」。
レイアウト内の各パーツ(TextViewやImageViewなど)を使いまわす。

典型的なViewHolderの使い方

class MyViewHolder(view: View): RecyclerView.ViewHolder(view) {
    val title: TextView = view.findViewById(R.id.title)
    val icon: ImageView = view.findViewById(R.id.icon)
}

// Adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
    return MyViewHolder(view)
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.title.text = dataSet[position].title
    // アイコンなどその他もセット
}

ポイントはView自体もViewHolderも再利用され、必要最小限だけ生成されることです。 Viewの構造は毎回作り直さずonBindViewHolder()の中でデータのみ差し替えすることでViewを再生成せずに済みます。

最後に

いかがでしたか? 今実装中のアプリでなぜかカクつくだとか動きが重いなぁと感じた方はレイアウトも疑ってみてください!

  • ネストを浅くする
  • layout_width / heightの使い分けを見直す
  • Viewの再利用を徹底する

これらを見直すことで「重い…」が「サクサク!」に変わるかもしれません。