メモリ管理のベスト プラクティス

このドキュメントは、Android アプリがメモリ管理に関するベスト プラクティスのガイダンス(アプリのメモリを管理するなど)に従って設計されていることを前提としています。

はじめに

メモリリークとは、不要になった割り当て済みメモリをコンピュータ プログラムが解放しない場合に発生するリソースリークの一種です。リークがあると、使用可能なメモリよりも多くのメモリをアプリケーションが OS にリクエストして、クラッシュを招く可能性があります。Android アプリでメモリリークが発生する場合、リソースが適切に解放されない、不要になったリスナーの登録が解除されないなど、さまざまな不適切な処理が原因として考えられます。

このドキュメントでは、コード内のメモリリークを防止、検出、解決するのに役立つベスト プラクティスを示します。このドキュメントの方法を試したうえで、Google の SDK にメモリリークが疑われる場合には、Google SDK の問題を報告する方法をご覧ください。

サポートにお問い合わせいただく前に

メモリリークを Google サポートチームにご報告いただく前に、このドキュメントに示されたベスト プラクティスとデバッグ手順を実践し、ご自身のコードに問題がないことをご確認ください。これらの手順を実践すると、問題が解決する可能性があり、解決しない場合でも、Google サポートチームが問題解決をサポートするうえで必要となる情報が生成されます。

メモリリークを防ぐ

次のベスト プラクティスを実践し、Google SDK を使ったコードにおけるメモリリークの一般的な原因を回避してください。

Android アプリ向けのベスト プラクティス

Android アプリにおいて、次のすべてを実践したことを確認します。

  1. 未使用のリソースを解放する
  2. 不要になったリスナーの登録を解除する
  3. 不要になったタスクをキャンセルする
  4. ライフサイクル メソッドを転送してリソースを解放する
  5. 最新バージョンの SDK を使用する

個々のベスト プラクティスの詳細については、以降のセクションをご覧ください。

未使用のリソースを解放する

Android アプリでリソースを使用する場合は、そのリソースが不要になった時点で必ず解放するようにします。そうしなければ、アプリで使い終わった後も、そのリソースがメモリを占拠します。詳しくは、Android ドキュメントのアクティビティのライフサイクルをご確認ください。

GeoSDKs で GoogleMap の古い参照を解放する

よくあるミスとして、NavigationView または MapView を使用してキャッシュに保存されると、GoogleMap はメモリリークを引き起こすことがあります。GoogleMap は、取得元の NavigationView または MapView と 1 対 1 の関係にあるため、GoogleMap をキャッシュに保存していないことを確認するか、NavigationView#onDestroy または MapView#onDestroy を呼び出したときに参照を解放する必要があります。NavigationSupportFragment、MapSupportFragment、またはこれらのビューをラップする独自のフラグメントを使用している場合は、Fragment#onDestroyView で参照を解放する必要があります。

class NavFragment : SupportNavigationFragment() {

  var googleMap: GoogleMap?

  override fun onCreateView(
    inflater: LayoutInflater,
    parent: ViewGroup?,
    savedInstanceState: Bundle?,
  ): View  {
    super.onCreateView(inflater,parent,savedInstanceState)
    getMapAsync{map -> googleMap = map}
  }

  override fun onDestroyView() {
    googleMap = null
  }
}

不要になったリスナーの登録を解除する

Android アプリで、イベント(ボタンのクリックやビュー状態の変化など)のリスナーを登録する場合は、アプリで対象イベントのモニタリングが不要になった時点で、そのリスナーの登録を必ず解除するようにします。そうしなければ、アプリで使い終わった後も、そのリスナーがメモリを占拠します。

たとえば、Navigation SDK を使うアプリで、到着イベントをリッスンするリスナー: addArrivalListener メソッドを呼び出して到着イベントをリッスンする場合には、その到着イベントのモニタリングが不要になった時点で、removeArrivalListener も呼び出す必要があります。

var arrivalListener: Navigator.ArrivalListener? = null

fun registerNavigationListeners() {
  arrivalListener =
    Navigator.ArrivalListener {
      ...
    }
  navigator.addArrivalListener(arrivalListener)
}

override fun onDestroy() {
  navView.onDestroy()
  if (arrivalListener != null) {
    navigator.removeArrivalListener(arrivalListener)
  }

  ...
  super.onDestroy()
}

不要になったタスクをキャンセルする

Android アプリで、ダウンロードやネットワーク リクエストなどの非同期タスクを始めたら、それが終わったときに、そのタスクを必ずキャンセルします。そうしないと、アプリがそのタスクを終えた後も、バックグラウンドで実行され続けます。

このベスト プラクティスについて詳しくは、Android ドキュメントのアプリのメモリを管理するをご覧ください。

ライフサイクル メソッドを転送してリソースを解放する

アプリで Navigation SDK や Maps SDK を使う場合は、ライフサイクル メソッド(太字で表示)を navView に転送し、そのリソースを必ず解放します。このリソースの解放は、Navigation SDK の NavigationView を使うか、Maps SDK か Navigation SDK の MapView を使うことで実施できます。また、NavigationViewMapView を直接使う代わりに、それぞれ SupportNavigationFragmentSupportMapFragment を使うこともできます。そのサポート フラグメントが、ライフサイクル メソッドの転送を処理します。

class NavViewActivity : AppCompatActivity() {

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

  override fun onSaveInstanceState(savedInstanceState: Bundle) {
    super.onSaveInstanceState(savedInstanceState)
    navView.onSaveInstanceState(savedInstanceState)
  }

  override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    navView.onTrimMemory(level)
  }

  /* Same with
    override fun onStart()
    override fun onResume()
    override fun onPause()
    override fun onConfigurationChanged(...)
    override fun onStop()
    override fun onDestroy()
  */
}

最新バージョンの SDK を使用する

Google SDK は、新機能の追加、バグの修正、パフォーマンスの改善などによって常に更新されています。これらの修正を受け取るには、アプリの SDK を常に最新の状態に保ってください。

メモリリークをデバッグする

前述の推奨事項の該当項目をすべて適用してもメモリリークが解消されない場合は、次のプロセスに沿ってデバッグしてください。

デバッグを始める前に、Android がメモリを管理する仕組みを知っておく必要があります。その情報については、Android のメモリ管理の概要をお読みください。

メモリリークのデバッグは、次のプロセスで行います。

  1. 問題を再現する。このステップは、メモリリークのデバッグに欠かせません。
  2. メモリが想定どおりに使用されているかチェックする。使用量が増えてリークのように見える場合、まずそれがアプリの実行に必要な増加ではないことを確認します。
  3. 上位レベルでデバッグする。デバッグに使えるユーティリティには複数の種類があります。次の 3 つの標準ツールセットは Android でのメモリの問題のデバッグに役立ちます。それは、Android Studio、Perfetto、Android Debug Bridge(adb)のコマンドライン ユーティリティです。
  4. アプリによるメモリの使用状況をチェックするヒープダンプと割り当てトラッキングを取得して、内容を分析します。
  5. メモリリークを修正する

次のセクションで、これらのステップについて詳しく説明します。

ステップ 1: 問題を再現する

問題を再現できない場合は、まずメモリリークにつながる可能性のある状況を考えます。問題が再現されている場合は、すぐにヒープダンプを確認して分析してもよいでしょう。しかし、アプリの起動時やランダムなタイミングでヒープダンプを取得した場合には、リークを引き起こす条件が揃っていない可能性があります。問題を再現する際は、次のようなさまざまな状況を想定して再現を試みます。

  • 有効になっている機能の組み合わせは?

  • リークを引き起こすユーザー操作のシーケンスは?

    • そのシーケンスが生まれる状況で問題の再現を複数回試したか?
  • アプリがライフサイクルのどの周期を巡っている状況なのか?

    • ライフサイクルのさまざまな周期で問題の再現を複数回試したか?

最新バージョンの SDK で問題を再現できることを確認してください。以前のバージョンで発生していた問題は修正済みの可能性があります。

ステップ 2: アプリでのメモリ使用量が想定どおりかどうかチェックする

どの機能も追加のメモリを必要とします。さまざまな状況をデバッグする場合は、その都度想定どおりにメモリが使用されているか、メモリリークなのかを見極めます。たとえば、機能やユーザータスクが異なる状況についてデバッグする場合は、次の可能性を考慮します。

  • リークの可能性が高い: 同じ状況を繰り返し再現すると、次第にメモリ使用量が増える。

  • メモリ使用量が想定どおりの可能性が高い: その状況が停止すると、メモリが解放される。

  • メモリ使用量が想定どおりに近い: これはメモリ使用量が一時期増えてから徐々に減る現象を指し、原因としては、キャッシュ サイズが制限されているか、他のプロセスによってメモリが使用されている可能性があります。

アプリの動作で見込まれるメモリ使用量の場合は、アプリのメモリを管理することで問題を解決できる可能性があります。この場合のヘルプ情報については、アプリのメモリ使用量を管理するをご覧ください。

ステップ 3: 上位レベルでデバッグする

メモリリークをデバッグする際は、上位レベルでデバッグを開始して、原因候補を絞り込んで掘り下げていきます。メモリリークが徐々に現れる場合は、まず、次に示す上位レベルのデバッグツールのいずれかを使って分析します。

Android Studio Memory Profiler

このツールを使うと、消費されているメモリをヒストグラムで視覚的に把握できます。ヒープダンプと割り当てトラッキングでも、これと同じインターフェースが使われます。これは標準的に使用が推奨されるツールです。詳しくは、Android Studio Memory Profiler をご覧ください。

Perfetto メモリカウンタ

Perfetto では、複数の指標のトラッキングをきめ細かく管理でき、そうした指標をすべて 1 つのヒストグラムに表せます。詳しくは、Perfetto メモリカウンタをご覧ください。

Perfetto のユーザー インターフェース

Android Debug Bridge(adb)コマンドライン ユーティリティ

Perfetto でトラッキングできる多くのことは、自分で直接クエリを実行する adb コマンドライン ユーティリティでもトラッキングできます。特に重要なコマンドラインとして、次の例を挙げておきます。

  • Meminfo では、ある時点での詳しいメモリ情報を確認できます。

  • Procstats では、重要な集約データの変遷がわかります。

ここに表示される重要な統計値は、アプリが長い間必要とする物理的なメモリ使用量の最大値(maxRSS)です。MaxPSS は maxRSS ほど精度が高くない場合があります。精度を高める方法については、adb shell dumpsys procstats --help –start-testing フラグの説明をご覧ください。

割り当てトラッキング

割り当てトラッキングは、メモリが割り当てられたスタック トレースを識別し、そのメモリが解放されたかどうかを明らかにします。このステップは、ネイティブ コードのリークをトラッキングする場合に特に有用です。スタック トレースを識別するこのツールは、問題の根本原因を短期間でデバッグしたり、再現方法を見定めたりするうえで効果的な手段です。割り当てトラッキングの使用手順については、割り当てトラッキングでネイティブ コード内のメモリをデバッグするをご覧ください。

ステップ 4: ヒープダンプでアプリのメモリ使用量をチェックする

メモリリークを見つける一つの方法は、アプリのヒープダンプを取得してリークを調べることです。ヒープダンプとは、アプリのメモリ内の全オブジェクトのスナップショットです。ヒープダンプは、メモリリークをはじめ、メモリに関する問題の診断に利用できます。

Android Studio では、GC で修正できないメモリリークを検出できます。ヒープダンプをキャプチャすると、まだリーチ可能であるにもかかわらず、すでに破棄されているアクティビティやフラグメントがないか、Android Studio がチェックします。

  1. ヒープダンプをキャプチャする
  2. ヒープダンプを分析してメモリリークを見つける
  3. メモリリークを修正する

詳しくは、次のセクションをご覧ください。

ヒープダンプをキャプチャする

ヒープダンプのキャプチャには、Android Debug Bridge(adb)や Android Studio Memory Profiler が使えます。

adb を使ってヒープダンプをキャプチャする

adb を使ってヒープダンプをキャプチャするには、次の手順を行います。

  1. Android デバイスをパソコンに接続します。
  2. コマンド プロンプトを開き、adb ツールがあるディレクトリに移動します。
  3. ヒープダンプをキャプチャするには、次のコマンドを実行します。

    adb shell am dumpheap my.app.name $PHONE_FILE_OUT

  4. そのヒープダンプを取得するには、次のコマンドを実行します。

    adb pull $PHONE_FILE_OUT $LOCAL_FILE.

Android Studio を使ってヒープダンプをキャプチャする

Android Studio Memory Profiler を使ってヒープダンプをキャプチャするには、Android でヒープダンプをキャプチャするセクションの手順を実施します。

ヒープダンプを分析してメモリリークを見つける

キャプチャしたヒープダンプは、Android Studio Memory Profiler を使って分析できます。それには、次の手順を実施します。

  1. Android Studio で Android プロジェクトを開きます。

  2. [Run](実行)、[Debug configuration](デバッグ構成)の順に選択します。

  3. [Android Profiler] タブを開きます。

  4. [Memory](メモリ)を選択します。

  5. [Open heap dump](ヒープダンプを開く)を選択し、生成したヒープダンプ ファイルを選択します。Memory Profiler に、アプリのメモリ使用量のグラフが表示されます。

  6. このグラフを使ってヒープダンプを分析します。

    • 使われなくなったオブジェクトを識別する。

    • 大量のメモリを使っているオブジェクトを識別する。

    • 各オブジェクトで使われているメモリの量を把握する。

  7. こうした情報を使ってメモリリークの原因を掘り下げるか見つけ出して、問題を修正します。

ステップ 5: メモリリークを修正する

メモリリークの原因が分かれば、その問題を修正できます。Android アプリのメモリリークを修正すると、アプリのパフォーマンスと安定性が向上します。状況に応じて細部は異なりますが、次の推奨事項が役立つ可能性があります。

その他のデバッグツール

前述のステップを完了しても、メモリリークの原因が見つからず解消できない場合には、次のツールをお試しください。

割り当てトラッキングでネイティブ コード内のメモリをデバッグする

ネイティブ コードを直接的に使っていない場合でも、一般的な Android ライブラリの中には、ネイティブ コードを使っているものがあります(Google SDK など)。メモリリークの原因がネイティブ コードにあるとお考えの場合は、そのデバッグに使用できる複数のツールがあります。Android Studioheapprofd(Perfetto にも対応)のいずれかを使った割り当てトラッキングは、メモリリークの原因候補を特定する効果的な手段であり、多くの場合は最短時間でデバッグできます。

割り当てトラッキングには、ヒープ内に機密情報を含めずに結果を知らせるという際立った利点もあります。

LeakCanary でリークを特定する

LeakCanary は、Android アプリ内のメモリリークの識別に効果を発揮するツールです。アプリで LeakCanary を使う方法について詳しくは、LeakCanary をご覧ください。

Google SDK を使って問題を報告する方法

このドキュメントの方法を試したうえで、Google の SDK にメモリリークが疑われる場合には、次の情報をできるだけ多くご用意のうえ、カスタマー サポートにご連絡ください。

  • メモリリークを再現するための手順。この手順で複雑なコーディングが必要になる場合には、代替手段として、問題が再現されるコードを Google のサンプルアプリにコピーして、問題のメモリリークを引き起こすのに必要な UI 操作の手順をお知らせいただくことも可能です。

  • 問題が再現されたアプリでキャプチャされたヒープダンプ。2 つの異なるタイミングでメモリ使用量が大幅に増えていることを示すヒープダンプをキャプチャします。

  • ネイティブ コードでのメモリリークが疑われる場合には、heapprofd で取得した割り当てトラッキングの出力結果をお送りください。

  • バグレポート: メモリリークの発生条件を再現した後のバグレポート。

  • メモリ関連クラッシュのスタック トレース(該当する場合)。

    重要な注意事項: 一般的に、メモリの問題をデバッグするにはスタック トレースだけでは不十分なので、別の種類の情報も必ずお送りください。