アプリの起動時間

ユーザーは、アプリの読み込みが速く、応答性が高いことを期待しています。起動が遅いアプリはこうした期待に応えられず、ユーザーを失望させます。この種のエクスペリエンスの低さは、ユーザーが Play ストアでアプリに低評価を付けたり、アプリの使用を完全にやめたりする結果につながります。

このページでは、アプリの起動時間を最適化するための情報を紹介します。たとえば、起動プロセスの内部的な概要や、起動パフォーマンスのプロファイリング方法、起動時によくある問題とその解決のためのヒントなどです。

アプリのさまざまな起動状態の理解

アプリの起動は、コールド スタート、ウォーム スタート、ホットスタートの 3 つの状態のいずれかで行われます。それぞれの状態は、アプリがユーザーに表示されるまでの時間に影響します。コールド スタートでは、アプリはゼロからスタートします。他の状態では、システムは実行中のアプリをバックグラウンドからフォアグラウンドに移動する必要があります。

Google では、常にコールド スタートを想定して最適化を行うことをおすすめします。そうすれば、ウォーム スタートとホットスタートのパフォーマンスも向上させることができます。

アプリが高速で起動するように最適化するには、上記の各状態において、システムレベルとアプリレベルで何が起こっているか、そしてそれらがどのように相互作用するかを理解することが重要です。

アプリの起動時間を決定するための重要な指標は、初期表示までの時間(TTID)完全表示までの時間(TTFD)の 2 つです。TTID は最初のフレームを表示するのにかかる時間で、TTFD はアプリが完全にインタラクティブになるまでの時間です。TTID はアプリが読み込み中であることをユーザーに示し、TTFD はアプリが実際に使用可能になったことを示すため、どちらも重要です。いずれかが長すぎると、アプリが完全に読み込まれる前にユーザーがアプリを終了してしまう可能性があります。

コールド スタート

コールド スタートとは、アプリをゼロからスタートさせることです。つまり、このスタートが実行されるまで、システムのプロセスはアプリのプロセスを作成します。コールド スタートが発生するのは、デバイスの起動後に初めてアプリを起動する場合や、システムがアプリを強制終了した後で起動する場合です。

システムとアプリは他の起動状態よりも多くの作業を行う必要があるため、コールド スタートは、起動時間を最小化するうえで最大の課題となっています。

コールド スタートの開始時に、システムは 3 つのタスクを実行します。

  1. アプリを読み込んで起動する。
  2. 起動直後にアプリの空白の開始ウィンドウを表示する。
  3. アプリプロセスを作成する。

システムがアプリプロセスを作成するとすぐに、アプリプロセスは次の各段階のタスクを実行します。

  1. アプリ オブジェクトを作成する。
  2. メインスレッドを起動する。
  3. メイン アクティビティを作成する。
  4. ビューをインフレートする。
  5. 画面のレイアウトを設定する。
  6. 初期描画を実行する。

アプリプロセスが最初の描画を完了すると、システム プロセスは表示されている背景ウィンドウを消去してメイン アクティビティに置き換えます。この時点で、ユーザーはアプリの使用を開始できます。

図 1 は、システム プロセスとアプリプロセスが互いに受け渡す作業を示しています。

図 1. アプリのコールド スタートに関する重要な部分を視覚的に表した図。

パフォーマンスの問題が発生する可能性があるのは、アプリの作成時とアクティビティの作成時です。

アプリの作成

アプリが起動する際に、アプリの最初の描画が完了するまで空白の開始ウィンドウが画面に表示されます。描画が完了した時点で、システム プロセスはアプリの開始ウィンドウをアプリ画面に切り替えて、ユーザーがアプリの操作を開始できるようにします。

アプリ内で Application.onCreate() をオーバーライドする場合、システムはアプリ オブジェクトで onCreate() メソッドを呼び出します。その後、アプリはメインスレッド(UI スレッド)を生成し、メイン アクティビティを作成するタスクをメインスレッドに実行させます。

この時点以降、システムレベルおよびアプリレベルのプロセスは、アプリ ライフサイクルのステージに従って進行します。

アクティビティの作成

アプリプロセスがアクティビティを作成した後、アクティビティは次のオペレーションを実行します。

  1. 値を初期化する。
  2. コンストラクタを呼び出す。
  3. アクティビティの現在のライフサイクル状態に応じて、Activity.onCreate() などのコールバック メソッドを呼び出す。

通常、onCreate() メソッドはオーバーヘッドが最も大きい作業(ビューの読み込みとインフレート、アクティビティの実行に必要なオブジェクトの初期化)を実行するため、読み込み時間に最も大きな影響を与えます。

ウォーム スタート

ウォーム スタートには、コールド スタートで実行されるオペレーションのサブセットが含まれます。また、ホットスタートに比べてオーバーヘッドが大きくなります。ウォーム スタートが発生する可能性があると見なされる状態は数多くあり、例として以下のようなものがあります。

  • ユーザーがアプリを終了したが、その後再び起動した。プロセスはまだ実行中の可能性がありますが、アプリは onCreate() の呼び出しを使用してアクティビティをゼロから再作成する必要があります。

  • システムがメモリからアプリを削除し、その後ユーザーがアプリを再起動した。プロセスとアクティビティは再起動が必要ですが、タスクでは onCreate() に渡された保存済みインスタンスの状態バンドルを利用できます。

ホットスタート

アプリのホットスタートは、コールド スタートよりもオーバーヘッドが小さいプロセスです。ホットスタートでは、システムがアクティビティをフォアグラウンドに移動します。アプリのすべてのアクティビティがまだメモリに存在していれば、アプリはオブジェクトの初期化、レイアウトの拡張、レンダリングを繰り返す必要がありません。

ただし、onTrimMemory() などのメモリ トリミング イベントでメモリの一部がパージされた場合、削除されたオブジェクトをホットスタート イベントの際に再作成する必要があります。

ホットスタートは、コールド スタートのシナリオと同じ画面上の動作を表示します。アプリがアクティビティのレンダリングを完了するまで、システム プロセスは空白の画面を表示します。

図 2. さまざまな起動状態とそれぞれのプロセスを示した図。各状態は描画される最初のフレームから始まります。

Perfetto でアプリの起動を特定する方法

アプリの起動に関する問題をデバッグする場合は、アプリの起動フェーズに具体的に何が含まれているかを確認しておくと役に立ちます。Perfetto でアプリの起動フェーズ全体を特定するには、次の手順を行います。

  1. Perfetto で、Android App Startups の派生指標を含む行を見つけます。表示されない場合は、デバイス上のシステム トレースアプリを使用してトレースをキャプチャしてみてください。

    図 3. Perfetto の Android App Startups の派生指標スライス。
  2. 関連付けられたスライスをクリックし、M キーを押してスライスを選択します。 スライスの周りに角かっこが表示され、所要時間が示されます。所要時間は [現在の選択] タブにも表示されます。

  3. 固定アイコンをクリックして Android App Startups 行を固定します。このアイコンは、行の上にポインタを置くと表示されます。

  4. 目的のアプリが表示されている行までスクロールし、最初のセルをクリックして行を展開します。

  5. W キー(S、A、D キーはそれぞれズームアウト、左移動、右移動)を押すと、メインスレッド(通常は一番上)にズームインします。

    図 4.アプリのメインスレッドの横にある Android App Startups の派生指標スライス。
  6. 派生指標スライスを使用すると、アプリの起動に何が含まれているかを正確かつ簡単に確認できるため、引き続き詳細なデバッグを行うことができます。

指標を使用してスタートアップを検査し、改善する

起動時のパフォーマンスを適切に診断するために、アプリの起動にかかる時間を示す指標をトラッキングできます。Android には、アプリに問題があることを通知する手段と、診断を手助けする手段がいくつか用意されています。Android Vitals は問題の発生を警告し、診断ツールは問題の診断を支援します。

起動時の指標を利用するメリット

Android は、初期表示までの時間(TTID)指標と完全表示までの時間(TTFD)指標を使用して、アプリのコールド スタートとウォーム スタートを最適化します。Android ランタイム(ART)は、これらの指標のデータを使用して、今後の起動を最適化するためにコードを効率的にプリコンパイルします。

起動速度が速くなると、ユーザーがアプリをより持続的に操作できるようになります。これにより、早期の終了、インスタンスの再起動、別のアプリへの移動を減らすことができます。

Android Vitals

Android Vitals は、アプリの起動時間が長すぎる場合に Google Play Console で警告を発することで、アプリのパフォーマンスの改善をサポートします。

Android Vitals は、アプリの起動時間が以下のような場合に、長すぎると判断します。

  • コールド スタートに 5 秒以上かかっている。
  • ウォーム スタートに 2 秒以上かかっている。
  • ホットスタートに 1.5 秒以上かかっている。

Android Vitals は、初期表示までの時間(TTID)指標を使用します。Google Play が Android Vitals のデータを収集する方法については、Google Play Console のドキュメントをご覧ください。

初期表示までの時間

初期表示までの時間(TTID)は、アプリの UI の最初のフレームを表示するのにかかる時間です。この指標は、コールド スタート中のプロセスの初期化、コールド スタートまたはウォーム スタート中のアクティビティの作成、最初のフレームの表示など、アプリが最初のフレームを生成するのにかかる時間を測定します。アプリの TTID を低く維持すると、アプリの起動が迅速にユーザーに表示されるため、ユーザー エクスペリエンスが向上します。TTID は、Android フレームワークによってすべてのアプリで自動的に報告されます。アプリの起動を最適化する際は、TTFD までの情報を取得するために reportFullyDrawn を実装することをおすすめします。

TTID は、次の一連のイベントを含む合計経過時間を表す時間値として測定されます。

  • プロセスを開始する。
  • オブジェクトの初期化。
  • アクティビティの作成と初期化。
  • レイアウトをインフレートする。
  • アプリを初めて描画する。

TTID を取得する

TTID を確認するには、Logcat コマンドライン ツールで、Displayed という値を含む出力行を検索します。この値は TTID で、次の例のようになります(TTID は 3s534ms です)。

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Android Studio で TTID を確認するには、図 5 に示すように、[フィルタ] プルダウンから [Logcat] ビューでフィルタを無効にしてから、Displayed 時間を見つけます。このログを提供するのはアプリ自体ではなくシステム サーバーであるため、フィルタを無効にする必要があります。

図 5. フィルタと logcat の Displayed 値を無効にしました。

Logcat 出力の Displayed 指標は、すべてのリソースが読み込まれて表示されるまでの時間をキャプチャしたものとは限りません。レイアウト ファイルで参照されていないリソースや、アプリがオブジェクトの初期化の過程で作成するリソースは除外されます。これらのリソースが除外されるのは、それらの読み込みがインライン プロセスであり、アプリの初期表示をブロックしないためです。

Logcat 出力の Displayed 行に、追加フィールドとして合計時間が表示されることがあります。次に例を示します。

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

この場合、最初の測定値は、最初に描画されたアクティビティのみを表します。total 時間の測定はアプリプロセスの起動時に開始されます。この値は、最初に起動されたものの、画面に何も表示しなかった別のアクティビティを含んでいる可能性があります。total 時間の測定値は、単一のアクティビティと合計起動時間に食い違いがある場合にのみ表示されます。

Android Studio では Logcat を使用することをおすすめしますが、Android Studio を使用していない場合は、adb シェル アクティビティ マネージャー コマンドでアプリを実行して TTID を測定することもできます。次の例をご覧ください。

adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

Displayed 指標は、以前と同様に Logcat の出力に表示されます。ターミナル ウィンドウには以下のように表示されます。

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

-c 引数と -a 引数は任意で、<category><action> を指定できます。

完全表示までの時間

完全表示までの時間(TTFD)とは、アプリがユーザーにとってインタラクティブになるまでの時間です。アプリの UI の最初のフレームと、最初のフレームが表示された後に非同期で読み込まれるコンテンツを表示するのにかかる時間が報告されます。通常、これはアプリから報告されるネットワークまたはディスクから読み込まれる主要なコンテンツです。つまり、TTID には TTID とアプリが使用可能になるまでの所要時間が含まれます。アプリの TTFD を低く維持すると、ユーザーがアプリを迅速に操作できるようになり、ユーザー エクスペリエンスが向上します。

TTID は、Choreographer がアクティビティの onDraw() メソッドを呼び出すとき、およびこのメソッドが初めて呼び出されることを認識したときに、システムが決定します。ただし、アプリごとに動作が異なるため、いつ TTFD を判定すべきか判断できません。TTFD を決定するには、完全に描画された状態に達したときに、アプリからシステムに通知する必要があります。

TTFD を取得する

TTFD を確認するには、ComponentActivityreportFullyDrawn() メソッドを呼び出して、完全に描画された状態を通知します。reportFullyDrawn メソッドは、アプリが完全に描画され、使用可能な状態になったことを報告します。TTFD は、システムがアプリの起動インテントを受け取ってから reportFullyDrawn() が呼び出されるまでの経過時間です。reportFullyDrawn() を呼び出さない場合、TTFD 値は報告されません。

TTFD を測定するには、UI とすべてのデータを完全に描画した後に reportFullyDrawn() を呼び出します。最初のアクティビティのウィンドウが最初に描画され、システムによって測定されたものとして表示される前には reportFullyDrawn() を呼び出さないでください。システムが測定した時刻を報告するためです。つまり、システムが TTID を検出する前に reportFullyDrawn() を呼び出すと、TTID と TTFD の両方が同じ値としてレポートされ、その値が TTID 値です。

reportFullyDrawn() を使用すると、Logcat は次のような出力を表示します。この例では、TTFD が 1 秒 54 ミリ秒です。

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

初期表示までの時間で説明したように、Logcat の出力には total 時間が含まれる場合があります。

表示時間が予想より短い場合は、起動プロセスのボトルネックの特定を試みることができます。

完全に描画された状態を実現したことがわかっている基本的なケースでは、reportFullyDrawn() を使用して完全に描画された状態を通知できます。ただし、完全に描画された状態に達する前にバックグラウンド スレッドがバックグラウンド処理を完了する必要がある場合は、TTFD をより正確に測定するために reportFullyDrawn() を遅延する必要があります。reportFullyDrawn() を遅らせる方法については、次のセクションをご覧ください。

起動時間の精度を改善する

アプリが遅延読み込みを実行していて、初期表示にすべてのリソースが含まれていない場合(アプリがネットワークから画像を取得する場合など)、アプリが使用可能になるまで reportFullyDrawn の呼び出しを遅らせることで、ベンチマークのタイミングの一部としてリストへのデータ入力を含めることができます。

たとえば、UI に RecyclerView や遅延リストなどの動的リストが含まれている場合、リストが最初に描画された後、つまり UI が完全に描画されたとマークされた後に完了するバックグラウンド タスクによって、データが入力されることがあります。この場合、リストへのデータ入力はベンチマークに含まれません。

リストへのデータ入力をベンチマーク時間に含めるには、getFullyDrawnReporter() を使用して FullyDrawnReporter を取得し、アプリコードでレポーターを追加します。バックグラウンド タスクでリストへのデータ入力が完了したらレポーターを解放します。

追加されたすべてのレポーターが解放されるまで、FullyDrawnReporterreportFullyDrawn() メソッドを呼び出しません。バックグラウンド プロセスが完了するまでレポーターを追加すると、リストにデータを入力するのにかかる時間も起動時間データに含まれるようになります。これにより、ユーザーにとってのアプリの動作が変わることはありませんが、起動のタイミング データに、リストへのユーザー入力にかかる時間が含まれるようになります。reportFullyDrawn() は、順序に関係なく、すべてのタスクが完了するまで呼び出されません。

次の例は、複数のバックグラウンド タスクを同時に実行し、それぞれに独自のレポーターを登録する方法を示しています。

Kotlin

class MainActivity : ComponentActivity() {

    sealed interface ActivityState {
        data object LOADING : ActivityState
        data object LOADED : ActivityState
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var activityState by remember {
                mutableStateOf(ActivityState.LOADING as ActivityState)
            }
            fullyDrawnReporter.addOnReportDrawnListener {
                activityState = ActivityState.LOADED
            }
            ReportFullyDrawnTheme {
                when(activityState) {
                    is ActivityState.LOADING -> {
                        // Display the loading UI.
                    }
                    is ActivityState.LOADED -> {
                        // Display the full UI.
                    }
                }
            }
            SideEffect {
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Java

public class MainActivity extends ComponentActivity {
    private FullyDrawnReporter fullyDrawnReporter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        fullyDrawnReporter = getFullyDrawnReporter();
        fullyDrawnReporter.addOnReportDrawnListener(() -> {
            // Trigger the UI update.
            return Unit.INSTANCE;
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

               fullyDrawnReporter.removeReporter();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

                fullyDrawnReporter.removeReporter();
            }
        }).start();
    }
}

アプリで Jetpack Compose を使用している場合は、次の API を使用して完全に描画された状態を示すことができます。

  • ReportDrawn: コンポーザブルですぐにインタラクションに対応できる準備が整っていることを示します。
  • ReportDrawnWhen: コンポーザブルでインタラクションの準備が整ったタイミングを示す list.count > 0 などの述語を受け取ります。
  • ReportDrawnAfter: 完了時にコンポーザブルでインタラクションの準備が整ったことを示す停止中のメソッドを受け取ります。
ボトルネックの特定

ボトルネックを探すには、Android Studio の CPU Profiler を使用します。詳細については、CPU Profiler を使用して CPU アクティビティを検査するをご覧ください。

また、アプリとアクティビティの onCreate() メソッド内でインライン トレースを行うことにより、潜在的なボトルネックに関する分析情報が得られます。インライン トレースの詳細については、Trace 関数のドキュメントとシステム トレースの概要をご覧ください。

よくある問題を解決する

このセクションでは、アプリの起動のパフォーマンスにしばしば影響する問題をいくつか取り上げます。ここで取り上げる問題は、主にアプリとアクティビティのオブジェクトの初期化、および画面の読み込みに関するものです。

アプリの初期化が遅い

アプリのコードで Application オブジェクトをオーバーライドし、そのオブジェクトを初期化する際に高負荷の作業や複雑なロジックを実行すると、起動のパフォーマンスが低下する可能性があります。また、まだ実行する必要のない初期化を Application のサブクラスが実行すると、アプリの起動時に時間が浪費される可能性があります。

場合によっては、一部の初期化はまったく不要です。たとえば、インテントに応じてアプリが実際に起動したときに、メイン アクティビティの状態情報を初期化する場合がこれに該当します。インテントでは、アプリは以前に初期化された状態データのサブセットのみを使用するためです。

アプリの初期化におけるその他の課題として、影響が大きい、または件数が多いガベージ コレクション イベントや、初期化と同時に発生するディスク I/O があります。これらは初期化プロセスをさらに遅らせます。Dalvik ランタイムでは、特にガベージ コレクションについて検討する必要があります。Android ランタイム(ART)ランタイムは、ガベージ コレクションを同時実行して、そのオペレーションの影響を最小限に抑えます。

問題を診断する

問題を診断する際は、メソッド トレースまたはインライン トレースを使用できます。

メソッド トレース

CPU Profiler を実行すると、最終的に callApplicationOnCreate() メソッドが com.example.customApplication.onCreate メソッドを呼び出すことがわかります。これらのメソッドの実行に時間がかかっていることをツールが示している場合は、さらに調査を進めて、そこで行われている作業を確認します。

インライン トレース

インライン トレースを使用して、次のような「問題の原因」を調べます。

  • アプリで初期設定されている onCreate() 関数。
  • アプリが初期化するグローバル シングルトン オブジェクト。
  • ボトルネックで発生する可能性があるディスク I/O、シリアル化解除、またはタイトループ。

問題の解決策

問題の原因が不要な初期化なのかディスク I/O なのかにかかわらず、解決策はオブジェクトの遅延初期化です。つまり、すぐに必要なオブジェクトのみを初期化します。グローバル静的オブジェクトを作成するのではなく、シングルトン パターンを採用して、アプリが最初に必要になったときにのみオブジェクトを初期化できるようにします。

また、Hilt のような依存関係注入フレームワークを使用して、最初に依存関係を注入するときにオブジェクトと依存関係を作成することを検討してください。

アプリがコンテンツ プロバイダを使用して起動時にアプリ コンポーネントを初期化する場合は、代わりにアプリの起動ライブラリの使用を検討してください。

アクティビティの初期化が遅い

アクティビティの作成には、オーバーヘッドが大きい多くの作業が頻繁に伴います。この作業を最適化してパフォーマンスを改善する機会は数多くあります。このような一般的な問題としては、次のようなものがあります。

  • 大規模または複雑なレイアウトの拡張。
  • ディスク I/O またはネットワーク I/O による画面描画のブロック。
  • ビットマップの読み込みとデコード。
  • VectorDrawable オブジェクトのラスター化。
  • アクティビティの他のサブシステムの初期化。

問題を診断する

このケースでも、メソッド トレースとインライン トレースの両方が有用です。

メソッド トレース

CPU Profiler を使用する際は、アプリの Application サブクラス コンストラクタと com.example.customApplication.onCreate() メソッドに注意してください。

これらのメソッドの実行に時間がかかっていることをツールが示している場合は、さらに調査を進めて、そこで行われている作業を確認します。

インライン トレース

インライン トレースを使用して、次のような「問題の原因」を調べます。

  • アプリで初期設定されている onCreate() 関数。
  • アプリが初期化するグローバル シングルトン オブジェクト。
  • ボトルネックで発生する可能性があるディスク I/O、シリアル化解除、またはタイトループ。

問題の解決策

潜在的なボトルネックは多数ありますが、一般的な問題と解決策を 2 つ示します。

  • ビュー階層が大きいほど、アプリが拡張する際に時間がかかります。この問題に対処するには、次の 2 つの手順を使用できます。
    • 冗長なレイアウトまたはネストされたレイアウトを減らして、ビュー階層をフラット化する。
    • 起動時に表示する必要がない UI 部分は拡張しないでください。代わりに、アプリがより適切なタイミングで拡張できるサブ階層のプレースホルダとして ViewStub オブジェクトを使用します。
  • リソースの初期化をすべてメインスレッドで行うことも、起動が遅くなることにつながる可能性があります。この問題には次の方法で対処できます。
    • リソースの初期化をすべて移動して、アプリが別のスレッドで遅延実行できるようにします。
    • ビューの読み込みと表示をアプリに許可し、ビットマップなどのリソースに依存する視覚的プロパティを後で更新する。

カスタム スプラッシュ画面

Android 11(API レベル 30)以下で、次のいずれかの方法を使用してカスタム スプラッシュ画面を実装していた場合、起動時間が長くなることがあります。

  • windowDisablePreview テーマ属性を使用して、起動時にシステムによって描画される最初の空白の画面をオフにする。
  • 専用の Activity を使用する。

Android 12 以降では、SplashScreen API に移行する必要があります。この API を使用すると、起動時間を短縮できます。また、次の方法でスプラッシュ画面を調整できます。

さらに、互換ライブラリは SplashScreen API をバックポートすることで、下位互換性を確保し、すべての Android バージョンでスプラッシュ画面表示の一貫したルック アンド フィールを作成します。

詳しくは、スプラッシュ画面の移行ガイドをご覧ください。