Androidアプリケーションのメモリ解析

公式ブログに「Memory Analysis for Android Applications 」という記事が投稿されたので、久々に訳してみた。誤訳などあったら教えて。ただ、画像貼るの面倒だったので、本家の方をクリックして拡大しつつながめてください。


ついでに、大昔に訳したメモリ3部作なんかも参考になるかも。

Androidアプリケーションのメモリ解析


Tim Brayが投稿


この投稿はPatrick Dubroyによるもの。PatrickはAndroidエンジニアで、プログラミングに関することや、ユーザビリティ、インタラクション・デザインについて個人的なブログを書いています


Dalvikランタイムはガベージコレクションが働くが、だからといってメモリ管理を無視してはいけない。携帯機器でのメモリ使用には特に注意しなければならない。携帯機器はメモリ制限がきつい。この記事では、Android SDKにあるメモリプロファイリングツールのいくつかを見てみる。それらはアプリケーションのメモリ使用量を減らすのに役立つだろう。


いくつかのメモリ使用での問題は自明だ。例えば、ユーザが画面をタッチするたびにメモリリークするアプリでは、OutOfMemoryErrorを引き起こし、いずれクラッシュするだろう。他の問題はもう少しやっかいで、ただアプリケーションとシステム全体を少しだけパフォーマンスを悪くするだけかもしれない。この劣化はアプリケーションでガベージコレクションがより頻繁により長く時間がかかることで起きる。

商売道具

Android SDKはアプリケーションのメモリ使用量をプロファイルする、主に2つの方法を提供する。DDMSでのAllocation Trackerタブと、ヒープダンプだ。Allocation Trackerは、ある決められた時間でどの種類のアロケーションが発生したのかを知りたいときに便利だ。しかし、アプリケーションのヒープ全体で何が起きているのかについては何も情報を提供しない。Allocation Trackerについての詳細はTracking Memory Allocationsを参照して欲しい。以下この記事では、より強力なメモリ解析ツールであるヒープダンプに着目する。


ヒープダンプはアプリケーションのヒープのスナップショットで、HPROFと呼ばれるバイナリ形式で保存される。DalivkもJavaにあるHPROFツールとよく似た形式を使用するが、まったく同じではない。実行中のAndroidアプリケーションのヒープダンプを生成するにはいくつかの方法がある。1つはDDMSでDump HPROF fileボタンを使う方法だ。ダンプが生成されるをタイミングをより正確にする必要があるなら、android.os.Debug.dumpHprofData() 関数をプログラム的に使用することでヒープダンプを生成することが可能だ。


ヒープダンプを解析するには、jhatEclipse Memory Analyzer (MAT)を使うことができる。しかし、Dalvik形式からJ2SE HPROF形式に.hprofファイルを変換する必要がある。そのためにAndroid SDKに含まれるhprof-convコマンドを使用する。例えば

hprof-conv dump.hprof converted-dump.hprof

メモリリークデバッグ

Dalvikランタイムでは、プログラマはメモリの確保と開放を明示的には行わない。だから、CやC++言語のように実際にメモリリークが発生することはない。コード中の「メモリリーク」は、不要になったオブジェクトへの参照を保持していることを指す。時には1つの参照がオブジェクトの巨大な集合がガベージコレクションされるのを防ぐこともある。


Android SDKからHoneycombのサンプルアプリケーションであるGalleryを例として、一通り見てみよう。これは単純なギャラリーアプリケーションで、新しいHoneycomb API群の中のいくつかの使い方を紹介する。サンプルコードをダウンロードしてビルドするには、こちらの説明を参照して欲しい。このアプリケーションにわざとメモリリークを追加して、デバッグ方法を見てみよう。


ネットワークから画像を引っ張ってくるようにアプリケーションを改造することを想定してみよう。アプリケーションの反応をよくするために、最近閲覧した画像を保持するキャッシュを実装するとしよう。それはContentFragment.javaに少しだけ変更を加えることで可能だ。クラスの先頭に、新しい静的変数を1つ加えてみる。

private static HashMap sBitmapCache = new HashMap();

これはロードしたビットマップをキャッシュする場所になる。続いて、updateContentAndRecycleBitmap()メソッドで、ロードする前にキャッシュをチェックし、ロード後にビットマップをキャッシュに追加するよう変更する。

void updateContentAndRecycleBitmap(int category, int position) {
    if (mCurrentActionMode != null) {
        mCurrentActionMode.finish();
    }
 
    // 描画するビットマップを取ってきて、ImageViewをアップデート
 
    // キャッシュにビットマップがすでにあるかチェック
    String bitmapId = "" + category + "." + position;
    mBitmap = sBitmapCache.get(bitmapId);
 
    if (mBitmap == null) {
        // キャッシュにないので、ビットマップをロードしてキャッシュに追加
        // 危険!何もリムーブせずに常にキャッシュに追加し続ける
        mBitmap = Directory.getCategory(category).getEntry(position)
                .getBitmap(getResources());
        sBitmapCache.put(bitmapId, mBitmap);
    }
    ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap);
}

ここに意図的にメモリリークを仕込んだ。まったくビットマップを取り除かずにキャッシュに追加している。現実のアプリケーションでは、何かの方法でキャッシュサイズに上限をつけるだろう。

DDMSを使ってヒープを調査

Dalvik Debug Monitor Server (DDMS)は主要なAndroidデバッグツールの1つだ。DDMSはADT Eclipseプラグインの一部で、SDKのtoolsディレクトリにスタンドアロン版がある。DDMSについての詳細はUsing DDMS*1を参照のこと。


ではDDMSを使ってこのアプリケーションのヒープの使い方を調べてみよう。DDMSの起動方法は2通りある。

DDMSのスクリーンショット

左のパネルでcom.example.android.hcgalleryプロセスを選択してから、ツールバーにあるShow heap updatesボタンをクリックする。次にDDMSのVM Heapタブに移動する。これは、ヒープメモリ使用状況の基本的な統計を表示、GCのたびに更新される。最初の更新を見るには、Cause GCボタンをクリックする。

Allocated 8MBをハイライトしたスクリーンショット

Allocatedコラムを見ると、現在の状態が8MBを少し超えていることがわかる。続いていくつかの写真を閲覧してみて、数値が上昇することを確認しよう。このアプリケーションには13枚の写真しか無いので、リークするメモリの総量は制限されている。場合によっては、これは最悪の種類のリークになる。なぜなら、リークしていることを示すOutOfMemoryErrorが発生することがないためだ。

ヒープダンプの生成

問題を追跡するためにヒープダンプを使ってみよう。DDMSツールバーのDump HPROF fileボタンをクリックして、ファイルを保存する場所を選択し、hprof-confを実行する。この例では、スタンドアロン版のMAT (version 1.0.1)を使う。これはMAT download siteで入手できる。


もし、プラグイン版のDDMSを含んでいるADTを動かしていて、MATをEclipseにインストールしているなら、dump HPROFボタンで自動的にhprof-convを使って変換が行われ、Eclipseの中で変換されたhprofファイルがMATによって開かれる。

MATを使ったヒープダンプの解析

MATを起動して、作成した変換済みHPROFファイルをロードする。MATは強力なツールで、すべての機能をここで説明するのはこの記事の範囲を超える。なので、ここではメモリリークを検出する方法を1つだけ紹介する。Histogramビューだ。Histogramビューは次の3つが表示される。インスタンス数でソート可能なすべてのクラスのリスト、shallow heap(すべてのインスタンスによって使用されている全メモリ容量)、retained heap(参照を保持している他のオブジェクトを含めた、すべてのインスタンスによって使用中になっている全メモリ容量)だ。

Eclipse Memory Analyzerのスクリーンショット

shallow heap(浅いヒープ)でソートすると、byteのインスタンスが一番上にくるのが分かる。Android 3.0 (Honeycomb)では、ビットマップオブジェクトの画素データはバイト配列に保持される(以前はDalvikヒープに保持されていた)。これらのオブジェクトのサイズからリークしたビットマップの裏側に居るメモリだと推測しても大丈夫だ。


byteクラスを右クリックして、List Objects - with incoming referencesを選択する。こうすると、ヒープにあるすべてのバイト配列のリストが生成され、Shallow Heap使用状況によってソートが可能になる。


大きなオブジェクトの1つを選んで、奥深く掘っていこう。そのオブジェクトを生かし続けている参照のつながりを、大元からオブジェクトまで辿れる。驚いたことに、ビットマップのキャッシュがあるじゃないか。

犯人こいつのスクリーンショット

MATはこれがリークであるかどうかを確実に判定できるものではない。なぜなら、これらのオブジェクトが必要かどうかを知らないからだ。プログラマだけがそれを出来る。この場合は、キャッシュが、アプリケーションの残りに比べると、大きなメモリ容量を消費している。なので、キャッシュのサイズを制限することを検討することになる。

MATでヒープダンプを比較

メモリリークデバッグには、2つの異なるタイミングの状態を比較するのが役立つことがある。そのためには、別々のHPROFファイルが必要だ。 (hrpof-convを使って変換することを忘れない様に)

2つのヒープダンプをMATで比較する方法は、少しややこしい。

  1. 最初のHPROFファイルを開く (File - Open Heap Dump)
  2. Histogramビューを開く
  3. Navigation Historyビュー(もし表示されていない場合はWindow - Navigation History)で、histogramを右クリックして、Add to Compare Basketを選択する
  4. 2つめのHPROFファイルを開いて、2と3を繰り返す
  5. Compare Basketビューに移動して、Compare the Resultsをクリックする。(ビューの右上にある赤い!アイコン)

まとめ

この記事では、Allocation Trackerとヒープダンプがアプリケーションのメモリ使用状況についてより良い理解を与えてくれることを示した。また、Eclipse Memory Analyzer(MAT)がアプリケーションのメモリリークを追跡するのに役立つことを示した。MATは強力なツールで、何が出来るかに付いて上っ面を引っ掻いただけだ。もっと学びたいのであれば、いくつかの読んでみるといい記事を推奨しておく。

*1:Track memory allocationsの超訳が参考になるかも