アプリ起動の分析と最適化

ユーザーのアプリに対する第一印象は起動時に決まります。アプリの起動では、ユーザーがアプリを使用するのに必要な情報を迅速に読み込んで表示できる必要があります。アプリの起動に時間がかかりすぎると、ユーザーは長い時間またされるためアプリを終了してしまう可能性があります。

Macrobenchmark ライブラリを使用して起動時間を測定することをおすすめします。このライブラリを使用することで、全体的な情報と詳細なシステム トレースが得られ、起動中の状況を正確に把握できます。

システム トレースからは、デバイス上の状況に関する有用な情報が得られます。この情報により、効果的にアプリ起動時の動作を把握し、最適化できる可能性のある領域を特定できます。

アプリの起動を分析するには、次の手順を行います。

起動を分析して最適化する手順

多くの場合、アプリはエンドユーザーにとって不可欠なリソースを起動時に読み込む必要があります。重要でないリソースは、起動が完了するまで読み込みを遅らせてもかまいません。

パフォーマンスとのトレードオフを行う際には、次の点を考慮してください。

  • Macrobenchmark ライブラリを使用して、各オペレーションにかかった時間を測定し、完了に時間がかかるブロックを特定します。

  • リソースを大量に使用しているオペレーションが、アプリの起動に不可欠なものであるかを確認します。そのオペレーションをアプリが完全に描画されるまで待機させられれば、起動時のリソース制約を最小限にできます。

  • このオペレーションをアプリ起動時に実行する必要があるかを確認します。多くの場合、不要なオペレーションの呼び出しは、古いコードやサードパーティ ライブラリから行われます。

  • 可能であれば、実行に長時間かかるオペレーションをバックグラウンドに移します。ただし、バックグラウンド プロセスでも起動時の CPU 使用率に影響する場合があります。

オペレーションについて十分調査すれば、読み込み時間と、アプリの起動に含める必要性とのトレードオフを判断できます。アプリのワークフローを変更するときは、回帰や破壊的変更の可能性に留意してください。

アプリの起動時間が満足できるレベルになるまで最適化と測定を繰り返します。詳細については、指標を使用した問題の検出と診断をご覧ください。

主要なオペレーションにかかった時間を測定して分析する

アプリの起動トレースを取得したら、それを確認して、bindApplicationactivityStart などの主要なオペレーションにかかった時間を測定します。これらのトレースを分析するには、Perfetto または Android Studio Profiler の使用をおすすめします。

アプリの起動にかかった全体の時間を見て、次のようなオペレーションを特定します。

  • 長い時間枠を占有する、最適化可能なオペレーション。ミリ秒単位でもパフォーマンスに影響します。たとえば、Choreographer の描画時間、レイアウトのインフレート時間、ライブラリの読み込み時間、Binder のトランザクションまたはリソースの読み込み時間を確認します。まずは、20 ミリ秒を超えるオペレーションをすべて確認しましょう。
  • メインスレッドをブロックするオペレーション。詳細については、Systrace レポートを操作するをご覧ください。
  • 起動時に実行する必要のないオペレーション。
  • 最初のフレームが描画されるまで遅延できるオペレーション。

これらの各トレースをさらに調査し、パフォーマンス ギャップを見つけてください。

メインスレッドでコストの高いオペレーションを特定する

ファイル I/O やネットワーク アクセスなど、コストの高いオペレーションはメインスレッドから切り離すことをおすすめします。アプリの起動時にも同じことが言えます。メインスレッドでコストの高いオペレーションを行うと、アプリが応答しなくなり、他の重要なオペレーションが遅延する可能性があるためです。コストの高いオペレーションがメインスレッドで行われているケースを特定するには、StrictMode.ThreadPolicy が役に立ちます。デバッグビルドで StrictMode を有効にして、できるだけ早い段階で問題を特定することをおすすめします。次の例をご覧ください。

Kotlin

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        ...
        if (BuildConfig.DEBUG)
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()
                    .penaltyDeath()
                    .build()
            )
        ...
    }
}

Java

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...
        if(BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                    new StrictMode.ThreadPolicy.Builder()
                            .detectAll()
                            .penaltyDeath()
                            .build()
            );
        }
        ...
    }
}

StrictMode.ThreadPolicy を使用すると、すべてのデバッグビルドでスレッド ポリシーが有効になり、スレッド ポリシーの違反が検出されるとアプリがクラッシュするため、スレッド ポリシー違反を見逃しにくくなります。

TTID と TTFD

アプリが最初のフレームを生成するのにかかる時間を確認するには、初期表示までの時間(TTID)を測定します。ただし、この指標は必ずしもユーザーがアプリの操作を開始できるようになるまでの時間を示しているわけではありません。一方、完全表示までの時間(TTFD)指標は、アプリが完全に使用できる状態になるまでに必要なコードパスを測定して最適化するために有効です。

アプリ UI が完全に描画されたときの通知に関する戦略については、起動時間の精度を改善するをご覧ください。

TTID と TTFD はどちらもそれぞれの領域で重要であるため、いずれも最適化する必要があります。TTID が短ければ、アプリが実際に起動されていることをユーザーが確認しやすくなります。一方、ユーザーがすぐにアプリを操作できるようにするためには、TTFD を短くすることが重要です。

スレッドの状態をすべて分析する

アプリの起動時間を選択して、スレッドのスライス全体を確認します。メインスレッドは常に応答可能である必要があります。

Android Studio ProfilerPerfetto などのツールを使用すると、メインスレッドの概要や各ステージにかかった時間の詳細を確認できます。Perfetto トレースの可視化について詳しくは、Perfetto UI のドキュメントをご覧ください。

メインスレッドのスリープ状態の主なチャンクを特定する

スリープに長い時間が費やされている場合は、アプリのメインスレッドが処理の完了を待っている可能性があります。マルチスレッド アプリの場合は、メインスレッドが完了を待っているスレッドを特定し、それらのオペレーションの最適化を検討します。また、不要なロックの競合によるクリティカル パスでの遅延を防ぐことも有効です。

メインスレッドのブロックと割り込み不可能なスリープを減らす

メインスレッドをブロック状態にしているケースをすべて見つけます。Perfetto と Studio Profiler では、スレッド状態のタイムラインにオレンジ色のインジケーターで示されます。それらのオペレーションを特定して、想定されたものか、あるいは回避可能なものかを調査し、必要ならば最適化します。

多くの場合、IO 関連の割り込み可能なスリープには改善の余地があります。IO を行う他のプロセスがあると、それが無関係なアプリであっても、トップのアプリが行っている IO と競合する可能性があります。

起動時間を短縮する

最適化の可能性を特定したら、以下のように起動時間の短縮に役立つ解決策を模索します。

  • コンテンツの読み込みを非同期にして遅延させ、TTID を短縮します。
  • バインダー呼び出しを行う関数の呼び出しを最小限にします。どうしても呼び出さなければならない場合も、呼び出しを繰り返さずに値をキャッシュに保存したり、ブロックしない処理をバックグラウンド スレッドに移動したりすることで、それらの呼び出しを最適化します。
  • アプリの起動が速いと感じられるように、すべての画面の読み込みが完了する前に、最低限レンダリングする必要のあるものを可能な限り早く表示します。
  • 起動プロファイルを作成してアプリに追加します。
  • Jetpack の App Startup ライブラリを使用して、アプリの起動時にコンポーネントの初期化を効率良く行えるようにします。

UI のパフォーマンスを分析する

アプリの起動には、スプラッシュ画面とホームページの読み込み時間が含まれます。アプリの起動を最適化するには、トレースを調べて UI の描画に要する時間を把握します。

初期化時の処理を制限する

一部のフレームは他のフレームよりも読み込みに時間がかかることがあります。こうしたフレームは、アプリにとって高コストな描画とみなされます。

初期化を最適化するには、次のようにします。

  • 低速なレイアウトパスを優先し、それらを改善対象とします。
  • カスタム トレース イベントを追加して Perfetto からの警告と Systrace からのアラートを調査し、コストの高い描画と遅延を削減します。

フレームデータを測定する

フレームデータの測定方法は複数あります。主に以下の 5 つの収集方法があります。

  • dumpsys gfxinfo を使用したローカルでの収集: dumpsys データに記録されたフレームのすべてが、アプリのレンダリングが遅い原因であったり、エンドユーザーに影響を与えたりするわけではありません。とはいえ、これを複数のリリース サイクルを通じて確認すると、パフォーマンスの全般的な傾向を把握するのに役立ちます。gfxinfoframestats を使用して UI のパフォーマンス測定値をテストに統合する方法について詳しくは、Android アプリのテストの基礎をご覧ください。
  • JankStats を使用したフィールドの収集: JankStats ライブラリを使用してアプリの特定の部分からフレーム レンダリング時間を収集し、使用しているデータの記録と分析を行います。
  • テストで Macrobenchmark を使用する(内部で Perfetto を使用)
  • Perfetto FrameTimeline: Android 12(API レベル 31)では、Perfetto トレースから、フレーム ドロップの原因となっている処理までの Frame Timeline 指標を収集できます。これがフレーム ドロップの原因を突き止めるための最初のステップになります。
  • Android Studio Profiler を使用した ジャンク検出

メイン アクティビティの読み込み時間を確認する

アプリのメイン アクティビティには、複数のソースから読み込まれる大量の情報が含まれている場合があります。ホームの Activity レイアウト、特にこのホーム アクティビティの Choreographer.onDraw メソッドを確認します。

  • 最適化のために、アプリが完全に描画されたことをシステムに通知する reportFullyDrawn を使用します。
  • Macrobenchmark ライブラリの StartupTimingMetric を使用して、アクティビティとアプリの起動を測定します。
  • フレーム ドロップを確認します。
  • レンダリングまたは測定に時間がかかっているレイアウトを特定します。
  • 読み込みに時間がかかっているアセットを特定します。
  • 起動時にインフレートされる不要なレイアウトを特定します。

メイン アクティビティの読み込み時間を最適化するには、次のソリューションを検討してください。

  • 最初のレイアウトは可能な限り基本的なものにします。詳細については、レイアウト階層の最適化をご覧ください。
  • カスタム トレースポイントを追加して、フレーム ドロップや複雑なレイアウトに関する詳細情報を取得します。
  • 起動時に読み込まれるビットマップ リソースの数とサイズを最小限にします。
  • レイアウトがすぐに VISIBLE にならない ViewStub を使用します。ViewStub は、サイズがゼロの目に見えない View です。これを使用すると、実行時にレイアウト リソースをゆっくりとインフレートできます。詳細については、ViewStub をご覧ください。

    Jetpack Compose を使用している場合は、ステータス情報に基づいて一部のコンポーネントの読み込みを遅らせることで、ViewStub と同様の効果が得られます。

    var shouldLoad by remember {mutableStateOf(false)}
    
    if (shouldLoad) {
     MyComposable()
    }
    

    shouldLoad を変更して、条件ブロック内のコンポーザブルを読み込みます。

    LaunchedEffect(Unit) {
     shouldLoad = true
    }
    

    これにより、最初のスニペットの条件ブロック内にあるコードを含む再コンポーズがトリガーされます。