アプリのスタートアップ時間

ユーザーは、アプリに対して応答性の高さと読み込みの速さを期待しています。起動が遅いアプリはこうした期待に応えられず、ユーザーを失望させます。この種のエクスペリエンスの低さは、ユーザーが 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() メソッドはオーバーヘッドが非常に大きい作業を実行するため、読み込み時間に多大な影響を及ぼします。その作業とは、ビューの読み込みおよび拡張と、アクティビティの実行に必要なオブジェクトの初期化です。

ウォーム スタート

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

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

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

ホットスタート

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

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

ホットスタートの画面上の表示動作は、コールド スタートの場合と同じです。

つまり、アプリがアクティビティのレンダリングを完了するまで、システム プロセスは空白の画面を表示します。

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

指標を使用した問題の検出と診断

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

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

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

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

Android Vitals

Android Vitals を使用すると、アプリの起動に時間がかかりすぎているときに、Google Play Console を介してアラートを受け取ることができます。これは、アプリのパフォーマンスの改善に役立ちます。Android Vitals は、次の場合にアプリの起動に時間がかかりすぎていると判断します。

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

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

初期表示までの時間

初期表示までの時間(TTID)指標は、アプリが最初のフレームを生成するのにかかる時間を測定します。これには、プロセスの初期化(コールド スタートの場合)、アクティビティの作成(コールド スタートとウォーム スタートの場合)、最初のフレームの表示が含まれます。

TTID の取得方法

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

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

レポートされるログの行の例を次に示します。

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</pre>

Displayed 指標は、前の方法と同様、logcat 出力に表示されます。ターミナル ウィンドウには次の情報も表示されます。

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

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

完全表示までの時間

完全表示までの時間(TTFD)指標は、アプリが完全なコンテンツ(最初のフレームの後に非同期で読み込まれるコンテンツを含む)で最初のフレームを生成するのにかかる時間を測定します。一般的に、これはネットワークから読み込まれるメインリストのコンテンツであり、アプリによってレポートされます。

TTFD の取得方法

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 プロファイラを実行すると、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 テーマ属性を使用して、起動時にシステムによって描画される最初の空白の画面をオフにする。
  • 専用のアクティビティを使用する。

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

さらに、互換ライブラリは SplashScreen API をバックポートして下位互換性を確保し、すべての Android バージョンで一貫性のあるスプラッシュ画面の表示を実現します。

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