アプリの起動時間

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

このドキュメントでは、アプリの起動時間を改善するための情報を提供します。最初に、起動プロセスの内部構造について説明します。次に、起動のパフォーマンスをプロファイリングする方法について説明します。最後に、起動時間に関するよくある問題を取り上げ、対処方法のヒントを紹介します。

アプリ起動の内部プロセスの概要

アプリの起動は、コールド スタート、ウォーム スタート、ホットスタートの 3 つの状態のいずれかで行われます。それぞれの状態は、アプリがユーザーに表示されるまでの時間に影響します。コールド スタートでは、アプリはゼロからスタートします。他の状態では、システムは実行中のアプリをバックグラウンドからフォアグラウンドに移動する必要があります。Google では、常にコールド スタートを想定して最適化を行うことをおすすめします。そうすれば、ウォーム スタートとホットスタートのパフォーマンスも向上させることができます。

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

コールド スタート

コールド スタートとは、アプリをゼロからスタートさせることです。コールド スタートの開始前にシステムのプロセスはアプリのプロセスを作成しません。コールド スタートが発生するのは、デバイスの起動後に初めてアプリを起動するときや、システムがアプリを強制終了した後で起動するときです。システムとアプリは他の起動状態よりも多くの作業を行う必要があるため、コールド スタートには、起動時間を最小化するうえで最も困難な課題が見い出されます。

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

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

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

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

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

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


図 1. アプリのコールド スタートの重要な部分の視覚的表現。

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

アプリの作成

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

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

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

アクティビティの作成

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

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

一般的に、onCreate() メソッドはオーバーヘッドが最も大きい作業を実行するため、読み込み時間に最も大きく影響します。そうした作業には、ビューの読み込みと拡張、アクティビティの実行に必要なオブジェクトの初期化などがあります。

ホットスタート

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

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

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

ウォーム スタート

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

  • ユーザーがアプリを終了したが、その後再び起動した。プロセスは引き続き実行されている可能性がありますが、アプリは onCreate() の呼び出しによりアクティビティをゼロから再作成する必要があります。
  • システムがメモリからアプリを削除し、その後ユーザーがアプリを再起動した。プロセスとアクティビティは再起動が必要ですが、タスクは onCreate() に渡された保存済みインスタンスの状態バンドルを利用できます。

問題の検出と診断

Android には、アプリに問題があることを通知する手段と、診断を手助けする手段がいくつか用意されています。Android Vitals は問題の発生を警告し、診断ツールは問題の診断を支援します。

Android Vitals

Android Vitals は、アプリの起動に時間がかかりすぎている場合、Play Console を介して警告を発します。これは、アプリのパフォーマンスを改善するのに役立ちます。Android Vitals は、次の場合にアプリの起動に時間がかかりすぎていると判断します。

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

1 日のセッションは、アプリが使用された日を意味します。

Android Vitals は、ホットスタートのデータを報告しません。Google Play が Android Vitals のデータを収集する方法については、Play Console のドキュメントをご覧ください。

起動時間の遅さの診断

起動時のパフォーマンスを適切に診断するために、アプリの起動にかかる時間を示す指標をトラッキングできます。

初期表示までの時間

Android 4.4(API レベル 19)以上では、logcat に Displayed という値を含む出力行が表示されます。この値は、プロセスを起動してから、対応するアクティビティを画面に描画し終えるまでにかかった時間を表します。この経過時間の中には、次の一連のイベントが含まれます。

  1. プロセスを起動する。
  2. オブジェクトを初期化する。
  3. アクティビティを作成して初期化する。
  4. レイアウトを拡張する。
  5. 初めてアプリを描画する。

レポートされるログの行は、次の例のようになります。

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

コマンドラインまたはターミナルで logcat の出力をトラッキングする場合、経過時間は簡単に見つけられます。Android Studio で経過時間を見つけるには、logcat ビューでフィルタを無効にしてください。このログを提供するのはアプリ自体ではなくシステム サーバーであるため、フィルタを無効にする必要があります。

適切な設定を行ったら、正確な用語を検索して簡単に経過時間を確認できます。図 2 はフィルタを無効にする方法を示しています。この例では、下から 2 行目に Displayed 時間を示す logcat 出力が表示されています。


図 2. フィルタを無効にして、logcat で Displayed の値を見つける

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

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

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

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

また、ADB Shell Activity Manager のコマンドを使用してアプリを実行することにより、初期表示にかかった時間を測定することもできます。次の例をご覧ください。

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> を指定できます。

完全表示までの時間

reportFullyDrawn() メソッドを使用して、アプリを起動してからすべてのリソースとビュー階層が完全に表示されるまでの経過時間を測定できます。これは、アプリが遅延読み込みを実行するケースで役立ちます。遅延読み込みでは、アプリはウィンドウの初期描画をブロックせず、代わりにリソースを非同期で読み込んでビュー階層を更新します。

遅延読み込みのためにアプリの初期表示にすべてのリソースが含まれない場合は、すべてのリソースとビューの完全な読み込みと表示を独立の指標と見なすことができます。たとえば、UI が完全に読み込まれてテキストの一部が描画されたものの、アプリがネットワークから取得する必要がある画像がまだ表示されていないケースがあります。

このようなケースの対処法として、手動で reportFullyDrawn() を呼び出し、アクティビティが遅延読み込みを完了したことをシステムに知らせることができます。このメソッドを使用した場合、logcat が表示する値は、アプリ オブジェクトが作成されてから reportFullyDrawn() が呼び出されるまでの経過時間です。logcat の出力例を次に示します。

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

初期表示までの時間で説明したとおり、logcat 出力には total 時間が含まれていることがあります。

表示時間が期待されるほど速くないことがわかったら、起動プロセスのボトルネックを特定する作業に進みます。

ボトルネックの特定

ボトルネックを見つける良い方法は、Android Studio の CPU Profiler を使用することです。詳細については、CPU Profiler で CPU のアクティビティを検証するをご覧ください。

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

よくある問題への対処

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

アプリの初期化が遅い

アプリのコードで Application オブジェクトをオーバーライドし、そのオブジェクトを初期化するときに高負荷の作業や複雑なロジックを実行すると、起動のパフォーマンスが低下する可能性があります。また、アプリのサブクラスがまだ実行する必要のない初期化を実行すると、アプリの起動時に時間が浪費される可能性があります。場合によっては、一部の初期化はまったく不要です。たとえば、インテントに応じてアプリが実際に起動したときに、メイン アクティビティの状態情報を初期化する場合がそうです。インテントでは、アプリは以前に初期化された状態データのサブセットのみを使用するからです。

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

問題の診断

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

メソッド トレース

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

インライン トレース

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

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

問題の解決策

問題の原因が不要な初期化なのかディスク I/O なのかにかかわらず、解決策にはオブジェクトの遅延初期化が必要です。つまり、すぐに必要なオブジェクトのみを初期化することです。たとえば、グローバルな静的オブジェクトを作成する代わりに、アプリがオブジェクトに最初にアクセスしたときにだけ初期化するシングルトン パターンを採用します。また、Dagger のような依存関係注入フレームワークを使用して、最初に依存関係を注入するときにオブジェクトと依存関係を作成することを検討してください。

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

アクティビティの作成には、多くの場合、たくさんの大きなオーバーヘッドが伴います。この作業を最適化してパフォーマンスを改善する機会は数多くあります。よくある問題を次に示します。

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

問題の診断

このケースでも、メソッド トレースとインライン トレースの両方が役に立ちます。

メソッド トレース

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

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

インライン トレース

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

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

問題の解決策

潜在的なボトルネックはたくさんありますが、次の 2 つのよくある問題と解決策を紹介します。

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

テーマ付きの起動画面

アプリの読み込みエクスペリエンスにテーマを設定して、アプリの起動画面のテーマをシステムのテーマではなくアプリの他の部分と同調させることがあります。そうすることで、アクティビティの起動の遅さを目立たなくすることができます。

テーマ付きの起動画面を実装する一般的な方法は、windowDisablePreview テーマ属性を使用して、アプリの起動時にシステム プロセスが描画する空白の初期画面を無効にすることです。しかし、このアプローチでは、プレビュー ウィンドウの表示を抑止しないアプリより起動時間が長くなる可能性があります。また、アクティビティの起動中、ユーザーはアプリからの反応がないまま待機しなければならず、アプリが正常に動作しているかどうか不安を感じるおそれがあります。

問題の診断

多くの場合、ユーザーがアプリを起動したときにレスポンスが遅いかどうかが診断基準となります。画面がフリーズするか、入力に対するレスポンスが停止しているように見える場合は問題があるといえます。

問題の解決策

プレビュー ウィンドウを無効にするのではなく、一般的なマテリアル デザインのパターンに従うことをおすすめします。アクティビティの windowBackground テーマ属性を使用すると、起動アクティビティ用のシンプルなカスタム ドローアブルを提供できます。

たとえば、次のように、新しいドローアブル ファイルを作成して、レイアウト XML およびアプリ マニフェスト ファイルからそのファイルを参照できます。

レイアウト XML ファイル:

    <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
      <!-- The background color, preferably the same as your normal theme -->
      <item android:drawable="@android:color/white"/>
      <!-- Your product logo - 144dp color version of your app icon -->
      <item>
        <bitmap
          android:src="@drawable/product_logo_144dp"
          android:gravity="center"/>
      </item>
    </layer-list>
    

マニフェスト ファイル:

    <activity ...
    android:theme="@style/AppTheme.Launcher" />
    

通常のテーマに戻る最も簡単な方法は、setTheme(R.style.AppTheme) を呼び出してから、super.onCreate()setContentView() を呼び出すことです。

Kotlin

    class MyMainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            // Make sure this is before calling super.onCreate
            setTheme(R.style.Theme_MyApp)
            super.onCreate(savedInstanceState)
            // ...
        }
    }
    

Java

    public class MyMainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // Make sure this is before calling super.onCreate
        setTheme(R.style.Theme_MyApp);
        super.onCreate(savedInstanceState);
        // ...
      }
    }