このドキュメントでは、アプリの主なパフォーマンスの問題を特定して修正する方法について説明します。
主なパフォーマンスの問題
アプリのパフォーマンス低下の原因となる問題は数多くありますが、アプリで注意すべき一般的な問題としては、次のようなものが挙げられます。
- 起動レイテンシ
起動レイテンシは、アプリアイコン、通知、その他のエントリ ポイントをタップしてから、ユーザーのデータが画面に表示されるまでにかかる時間です。
アプリで次の起動目標を目指します。
500 ミリ秒未満でコールド スタート。コールド スタートは、起動中のアプリがシステムのメモリに存在しない場合に発生します。これは、再起動後、あるいはユーザーまたはシステムによるアプリプロセスの強制終了後、最初にアプリを起動したときに発生します。
一方、ウォーム スタートはアプリがすでにバックグラウンドで実行されているときに発生します。コールド スタートでは、ストレージからすべてを読み込んでアプリを初期化する必要があるため、システムの負荷が最も多くなります。コールド スタートにかかる時間は、500 ミリ秒以下を目指してください。
レイテンシの中央値に非常に近い P95 レイテンシおよび P99 レイテンシ。アプリの起動に時間がかかると、ユーザー エクスペリエンスが低下します。アプリ起動のクリティカル パスにプロセス間通信(IPC)や不要な I/O があると、ロック競合が発生し、不整合が生じることがあります。
- スクロール ジャンク
「ジャンク」という用語は、システムが要求された頻度(60 Hz 以上)で画面に描画するフレームを時間内にビルド、提供できない場合に発生する視覚的な中断を表します。ジャンクはスクロール時に最も顕著に現れ、スムーズにアニメーション化されるべきフローで中断が発生します。アプリがコンテンツをレンダリングするためにかかる時間がシステム上のフレーム継続期間よりも長いため、1 つ以上のフレームの途中で動きが一時停止し、ジャンクが現れます。
アプリは 90 Hz のリフレッシュ レートをターゲットにする必要があります。従来のレンダリング レートは 60 Hz ですが、新しいデバイスの多くは、スクロールなどのユーザー操作中に 90 Hz モードで動作します。一部のデバイスでは、さらに高いレート(120 Hz まで)がサポートされています。
ある時点でデバイスが使用しているリフレッシュ レートを確認するには、[開発者向けオプション] > [リフレッシュ レートの表示]([デバッグ] セクション内)を使用してオーバーレイを有効にします。
- スムーズでない遷移
タブの切り替えや新しいアクティビティの読み込みなどの操作中に発生します。このタイプの遷移は、滑らかなアニメーションでなければならず、遅延や視覚的なちらつきを含んではいけません。
- 電力の非効率性
負荷がかかると電力が消費されます。不要な負荷がかかるとバッテリー駆動時間が短くなります。
コードに新しいオブジェクトを作成することで発生するメモリ割り当てが原因で、システムに大きな負荷がかかることがあります。これは、割り当て自体が Android ランタイム(ART)の労力を必要とするためだけでなく、後でそのオブジェクトを解放する場合(「ガベージ コレクション」)に時間と労力が必要になるためでもあります。割り当てとコレクションはどちらも、特に一時オブジェクトについては、以前に比べてはるかに高速かつ効率的です。以前は、可能な限りオブジェクトの割り当てを避けることがベスト プラクティスでしたが、アプリとアーキテクチャに最も適したものにすることをおすすめします。ART の能力を考えると、コードの管理が困難になるリスクを冒してまで割り当てを控えることはおすすめしません。
ただし、労力が必要になるため、内部ループで多くのオブジェクトを割り当てると、パフォーマンスの問題を引き起こす可能性があることに留意してください。
問題を特定する
パフォーマンスの問題を特定して修正するために、次のワークフローをおすすめします。
- 以下のクリティカル ユーザー ジャーニーを特定して検査します。
- 一般的な起動フロー(ランチャーや通知を含む)。
- ユーザーがデータをスクロールする画面。
- 画面間の遷移。
- ナビゲーションや音楽の再生のような長時間実行フロー。
- 次のデバッグツールを使用して、フローで何が起こっているかを検査します。
- Perfetto: 正確なタイミング データを使用して、デバイス全体で何が起こっているかを確認できます。
- Memory Profiler: ヒープで発生しているメモリ割り当てを確認できます。
- Simpleperf: 特定の期間にどの関数呼び出しが最も多く CPU を使用しているかを示すフレームグラフを表示します。Systrace で時間がかかっているものを特定しても、その原因がわからなかった場合、Simpleperf が追加情報を提供します。
こうしたパフォーマンスの問題を確認してデバッグするには、個々のテスト実行を手動でデバッグすることが重要です。集計データを分析しても、前の手順を置き換えることはできません。ただし、ユーザーに実際に何が表示されているかを理解し、いつ回帰が発生する可能性があるかを特定するには、自動テストとフィールドで指標の収集をセットアップすることが重要です。
- 起動フロー
- フィールド指標: Google Play Console の起動時間
- ラボテスト: Macrobenchmark を使用して起動をテストする
- ジャンク
- フィールド指標
- Google Play Console のフレーム指標: Google Play Console では、指標を特定のユーザー ジャーニーに絞り込むことはできません。アプリ全体のジャンクが報告されるだけです。
FrameMetricsAggregator
によるカスタム測定:FrameMetricsAggregator
を使用して、特定のワークフロー中にジャンク指標を記録できます。
- ラボテスト
- Macrobenchmark によるスクロール。
- Macrobenchmark は、単一のユーザー ジャーニーを囲む
dumpsys gfxinfo
コマンドを使用してフレーム時間を収集します。これにより、特定のユーザー ジャーニーに対するジャンクの変化を把握します。RenderTime
指標は、フレームの描画にかかる時間を強調するものであり、回帰または改善の特定では、ジャンクのあるフレームの数よりも重要です。
- フィールド指標
アプリリンクの検証に関する問題
アプリリンクは、ウェブサイトの URL に基づくディープリンクで、ウェブサイトに属していることが確認されたものです。以下は、App Link の検証が失敗する原因となる可能性があります。
- インテント フィルタのスコーピング: アプリが応答できる URL のインテント フィルタにのみ
autoVerify
を追加します。 - 未確認のプロトコル切り替え: 未確認のサーバーサイド リダイレクトとサブドメイン リダイレクトはセキュリティ リスクと見なされ、検証に失敗します。これにより、すべての
autoVerify
リンクが失敗します。たとえば、HTTPS リンクを検証せずに、HTTP から HTTPS にリンクをリダイレクトすると(example.com から www.example.com など)、検証が失敗する可能性があります。インテント フィルタを追加して、アプリリンクを検証してください。 - 検証できないリンク: テスト目的で検証できないリンクを追加すると、システムがアプリのアプリリンクを検証しなくなる可能性があります。
- 信頼できないサーバー: サーバーがクライアント アプリに接続できることを確認します。
パフォーマンス分析向けにアプリをセットアップする
正確で再現可能かつ実用的なベンチマークをアプリから取得するには、適切なセットアップが不可欠です。ノイズの原因を抑制しながら、できるだけ本番環境に近いシステムでテストします。以降のセクションでは、テストのセットアップを準備するための、APK とシステム固有の手順をいくつか紹介します。一部はユースケース固有のものです。
トレースポイント
アプリは、カスタム トレース イベントを使用してコードをインストルメント化できます。
トレースをキャプチャしている間、トレースにはセクションごとにわずかなオーバーヘッド(約 5 マイクロ秒)が発生するため、すべてのメソッドにトレースを適用することは避けてください。0.1 ミリ秒を超える大きな単位の処理をトレースすることで、ボトルネックに関する重要な分析情報を得ることができます。
APK に関する考慮事項
デバッグ バリアントは、スタック サンプルのトラブルシューティングやシンボル化に役立ちますが、パフォーマンスに著しい影響を与えます。Android 10(API レベル 29)以降を搭載しているデバイスでは、マニフェストで profileable android:shell="true"
を使用して、リリースビルドでのプロファイリングを有効にできます。
本番環境レベルのコード圧縮構成を使用します。アプリで使用するリソースによっては、これがパフォーマンスに大きな影響を与える可能性があります。一部の ProGuard 構成ではトレースポイントが削除されるため、テストを実行する構成でこれらのルールを削除することを検討してください。
コンパイル
デバイス上のアプリを既知の状態(通常はシンプルさのために speed
、本番環境のパフォーマンスに近づけるために speed-profile
)にコンパイルします(ただし、この場合、アプリのウォームアップとプロファイルのダンプ、またはアプリのベースライン プロファイルのコンパイルが必須です)。
speed
と speed-profile
の両方で、dex から解釈されて実行されるコードの量が減り、その結果、バックグラウンドのジャストインタイム(JIT)コンパイルの量が減り、干渉が大幅に軽減されます。speed-profile
のみが、dex からのランタイム クラスの読み込みの影響を軽減します。
次のコマンドは、speed
モードを使用してアプリケーションをコンパイルします。
adb shell cmd package compile -m speed -f com.example.packagename
speed
コンパイル モードでは、アプリのメソッドが完全にコンパイルされます。speed-profile
モードでは、アプリの使用中に収集された利用コードパスのプロファイルに沿ってアプリのメソッドとクラスがコンパイルされます。プロファイルを一貫して正確に収集することは難しいため、使用する場合は、想定どおりに収集されていることを確認してください。プロファイルは次の場所にあります。
/data/misc/profiles/ref/[package-name]/primary.prof
システムに関する考慮事項
低レベルと高忠実度の測定では、デバイスを調整します。同じデバイス、同じ OS バージョンで、A/B 比較を実行します。同じデバイスタイプであっても、パフォーマンスが大幅に異なる場合があります。
ユーザーに root 権限のあるデバイスでは、Microbenchmark に lockClocks
スクリプトを使用することを検討してください。特に、これらのスクリプトは次の処理を行います。
- CPU を固定周波数で配置する。
- 小さなコアを無効にして、GPU を構成する。
- サーマル スロットリングを無効にする。
lockClocks
スクリプトは、アプリの起動、DoU テスト、ジャンクテストなど、ユーザー エクスペリエンスに重点を置いたテストに使用することはおすすめしませんが、Microbenchmark テストではノイズを減らすために不可欠な場合があります。
可能であれば、Macrobenchmark などのテスト フレームワークを使用することを検討してください。測定時のノイズを減らし、不正確な測定を防ぐことができます。
アプリの起動が遅い: 不要なトランポリン アクティビティ
トランポリン アクティビティでは、アプリの起動時間が不必要に長くなる可能性があり、アプリがそれを行っているかどうかを認識することが重要です。次のトレース例に示すように、最初のアクティビティによってフレームが描画されることなく、1 つの activityStart
の直後に別の activityStart
が続きます。
これは、通知エントリポイントと通常のアプリ起動エントリポイントの両方で発生し、多くの場合はリファクタリングで対処できます。たとえば、別のアクティビティが実行される前にそのアクティビティを使用してセットアップを行う場合は、そのコードを再利用可能なコンポーネントまたはライブラリに分解します。
頻繁に GC をトリガーする不要な割り当て
Systrace では、ガベージ コレクション(GC)が想定よりも頻繁に発生することがあります。
次の例では、長時間実行オペレーション中の 10 秒ごとに、アプリが時間の経過とともに不必要に、一貫して割り当てを行っている可能性があることを示しています。
また、Memory Profiler を使用する場合、特定のコールスタックが割り当ての大部分を行っていることがわかります。コードの保守が困難になる可能性があるため、すべての割り当てを積極的に排除する必要はありません。代わりに、割り当てのホットスポットに取り組むことから始めます。
ジャンクのあるフレーム
グラフィック パイプラインは比較的複雑で、最終的にフレーム落ちが発生するかどうかの判断には微妙な違いが生じる可能性があります。場合によっては、プラットフォームがバッファリングを使用してフレームを「レスキュー」できます。しかし、その微妙な違いの大部分を無視して、アプリの観点から問題のあるフレームを特定できます。
アプリからの処理をほとんど必要とせずにフレームが描画されているとき、60 FPS デバイスでは Choreographer.doFrame()
トレースポイントは 16.7 ミリ秒周期で発生します。
ズームアウトしてトレースを移動すると、完了までにもう少し時間がかかるフレームが表示されることがありますが、割り当てられている 16.7 ミリ秒よりも時間がかかることはないため問題ありません。
図 5 に示すように、この規則的な頻度が中断された場合は、ジャンクのあるフレームになります。
これらを特定する練習をしましょう。
場合によっては、どのビューがインフレートされているか、または RecyclerView
が何を行っているかに関する詳細情報を得るために、トレースポイントにズームインする必要があります。また、さらなる調査が必要となることもあります。
ジャンクのあるフレームを特定して原因をデバッグする方法について詳しくは、遅いレンダリングをご覧ください。
RecyclerView に関するよくある間違い
RecyclerView
のバッキング データ全体を不必要に無効にすると、フレームのレンダリング時間が長くなり、ジャンクが発生する可能性があります。代わりに、更新が必要なビューの数を最小限に抑えるために、変更されたデータのみを無効にします。
コストのかかる notifyDatasetChanged()
呼び出し(コンテンツを完全に置き換えるのではなく更新する)を回避する方法については、動的データを提示するをご覧ください。
すべてのネストされた RecyclerView
が適切にサポートされていない場合、内部の RecyclerView
が毎回完全に再作成される可能性があります。すべてのネストされた内部 RecyclerView
に対して、すべての内部 RecyclerView
間でビューをリサイクルできるように、RecycledViewPool
を設定する必要があります。
十分なデータをプリフェッチしなかったり、適切なタイミングでプリフェッチしなかったりすると、ユーザーがサーバーから追加のデータを待たなければならないときに、スクロール リストの一番下まで到達しづらくなる可能性があります。フレームのデッドラインには間に合うため、これは厳密にはジャンクではありませんが、ユーザーがデータを待つ必要がないようにプリフェッチのタイミングと量を変更すると、ユーザー エクスペリエンスが大幅に改善する可能性があります。
アプリをデバッグする
アプリのパフォーマンスをデバッグする方法はいくつかあります。システム トレースおよび Android Studio プロファイラの使用の概要については、次の動画をご覧ください。
Systrace を使用してアプリの起動をデバッグする
アプリの起動プロセスの概要については、アプリの起動時間をご覧ください。システム トレースの概要については、次の動画をご覧ください。
起動タイプを明確にするには、次の段階で対応します。
- コールド スタート: 保存済み状態のない新しいプロセスの作成から開始します。
- ウォーム スタート: プロセスを再利用するか、保存済み状態からプロセスを再作成してアクティビティを再作成します。
- ホット スタートアップ: アクティビティを再起動し、インフレートで開始します。
デバイス上のシステム トレース アプリを使用して Systrace をキャプチャすることをおすすめします。Android 10 以降の場合は、Perfetto を使用します。Android 9 以前の場合は、Systrace を使用します。ウェブベースの Perfetto トレース ビューアでトレース ファイルを表示することもおすすめします。詳細については、システム トレースの概要をご覧ください。
次のような点を確認します。
- モニターの競合: モニター保護されたリソースの競合により、アプリの起動が大幅に遅れることがあります。
同期バインダー トランザクション: アプリのクリティカル パスに存在する不要なトランザクションを探します。必要なトランザクションが高額な場合は、関連するプラットフォーム チームと連携して改善を検討してください。
同時実行 GC: よく起こることで、影響もそれほど大きくはありません。ただし、頻繁に目にする場合は、Android Studio の Memory Profiler で調査することをおすすめします。
I/O: 起動中に実行された I/O を確認し、長い停滞がないかをチェックします。
他のスレッドでの重要なアクティビティ: UI スレッドの妨げになる可能性があるため、起動時のバックグラウンド処理に注意します。
アプリの起動指標のレポートを改善するには、アプリの観点で起動が完了したときに reportFullyDrawn
を呼び出すことをおすすめします。reportFullyDrawn
の使用方法については、全画面表示までの時間のセクションをご覧ください。Perfetto トレース プロセッサを使用して RFD で定義された開始時間を抽出すると、ユーザーに表示されるトレース イベントが出力されます。
デバイスでシステム トレースを使用する
システム レベルのアプリである System Tracing を使用して、デバイスのシステム トレースをキャプチャできます。このアプリを使用すると、デバイスを接続したり adb
に接続したりすることなく、デバイスからトレースを記録できます。
Android Studio の Memory Profiler を使用する
Android Studio の Memory Profiler を使用すると、メモリリークや不適切な使用パターンによって引き起こされるメモリ負荷を調べることができます。オブジェクトの割り当てをリアルタイムで確認できます。
アプリのメモリの問題を修正するには、Memory Profiler を使用して GC が発生する理由と頻度をトラッキングします。
アプリのメモリをプロファイリングする手順は次のとおりです。
メモリの問題を検出する。
注目するユーザー ジャーニーのメモリ プロファイリング セッションを記録します。図 7 に示すように、オブジェクト数が増加し、最終的に図 8 に示すように GC につながるかどうかを確認します。
メモリ負荷を増大させているユーザー ジャーニーを特定したら、メモリ負荷の根本原因を分析します。
メモリ負荷のホットスポットを診断する。
図 9 のように、タイムラインで範囲を選択し、[割り当て] と [シャローサイズ] の両方を可視化します。
このデータは、複数の項目で並べ替えを行えます。各ビューを利用して問題を分析する例をいくつか示します。
クラスで並べ替え: キャッシュに保存(メモリプールから再利用)すべきオブジェクトを生成しているクラスを見つけ出す場合に便利です。
たとえば、アプリが「Vertex」というクラスのオブジェクトを毎秒 2,000 個作成していた場合、割り当て数は毎秒 2,000 個ずつ増加し、クラスで並べ替えるとそれがわかります。これらのオブジェクトを再利用してガベージの生成を回避する場合は、メモリプールを実装します。
コールスタックで並べ替え: メモリが割り当てられているホットパス(ループ内や、大量の割り当て処理を行う特定の関数内など)を見つける場合に便利です。
Shallow Size: オブジェクト自体のメモリのみをトラッキングします。主にプリミティブ値で構成される単純なクラスのトラッキングに役立ちます。
Retained Size: オブジェクトと、そのオブジェクトのみが参照する参照に起因するメモリの合計サイズを示します。複雑なオブジェクトによるメモリ負荷をトラッキングする場合に使用します。この値を取得するには、図 10 に示すように完全なメモリダンプを取得します。図 11 に示すように、[Retained Size] が列として追加されます。
最適化の効果を測定する。
GC はより明確で、メモリ最適化の影響を測定しやすいため、最適化によりメモリ負荷が軽減されると、GC が少なくなります。
最適化の影響を測定するには、プロファイラのタイムラインで GC 間の時間を測定します。GC 間の時間が長くなっていることがわかります。
メモリ改善の最終的な効果は次のとおりです。
- アプリでメモリ負荷が常に発生していない場合、メモリ不足によるシャットダウンは減少する可能性があります。
- GC を減らすことで、特に P99 でジャンク指標が改善されます。これは、GC が CPU の競合を引き起こし、その間のレンダリング処理を遅延させる可能性があるものだからです。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- アプリの起動の分析と最適化 {:#app-startup-analysis-optimization}
- フリーズしたフレーム
- Macrobenchmark を作成する