2012年11月19日月曜日

Android プログラマの為の Eclipse Memory Analyzer Tool 入門

Andoid アプリ作ってる/はじめたけど、まだ MAT を使ったことがない方

  • MAT を使ってみようした事はあるものの、画面から難しそうな雰囲気を察知し、起動10秒後にはそっとタブを閉じてしまった経験がある方
  • DDMS の基本的な使い方を理解している方

Eclipse Memory Analyzer ってなに?

Eclipse Project の元で開発されている、メモリ使用量を分析する為のソフトウェアです。
Java VM 上でどのオブジェクトが沢山生成されているか、メモリ使用量が大きいか、などを分析してくれるツールです。Android SDK 専用のツールという訳ではなく、Java 開発全般で使う事ができます。


配布形態としては Eclipse Plugin 形式と、単独で起動する Standalone 形式があるので、まだインストールされていない方は、下記の記事等を参考にお好みの方をインストールしてください。
※この記事では Plugin 形式を想定して説明します。

おさえる基本事項 3 つ

MAT はとても強力なツールではあるものの、機能が多く若干複雑で取っつきにくい印象があります。
その為、この記事では MAT のすべての機能を網羅するのではなく、ポイントを基本的な3つだけに絞り紹介します。

  • Leak Suspects - リークしてそうなのはこいつらだ
  • Dominator tree - 大きいオブジェクトを一覧で見よう
  • Path to GC Roots - こいつは誰が参照してる?

リークを特定する為の基本的な流れとしては、Leak Suspects で怪しいところを教えてもらい、Dominator Tree*1 で怪しいオブジェクトを一覧 & 構造を調べ、Path to GC Roots でリーク元を特定する、という様な流れになります。


では、実際にメモリリークを持つプロジェクトの実例を交えながら見ていってみます。

メモリリークのサンプル Activity

簡単な例ですが、下記の様なメモリリークの問題を含んでいる Activity を用意し、実際に MAT を使ってメモリリークを特定してみます。
このプロジェクトは github でもホストしていますので、良ければ clone して手元で試してみてください。


https://github.com/tlync/eclipse-memory-analyzer-demo

...  public class MemoryAnalyzerDemoActivity extends Activity {        private static SomeInnerClass innerClass;        @SuppressWarnings("unused")      private byte[] someBigObject = new byte[1024 * 1024 * 3];        @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.main);                    ImageView background = (ImageView) findViewById(R.id.background);          background.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.droid));            if (innerClass == null) {              innerClass = new SomeInnerClass();          }          innerClass.doSomething();      }        class SomeInnerClass {          public void doSomething() {              Toast.makeText(getApplicationContext(), "Do something", Toast.LENGTH_SHORT).show();          }      }  }  ...  

メモリリークを起こしてみる

上記のプロジェクトを起動し、DDMS を開き、対象のプロセスを選択すると、起動直後のメモリ使用状況は 5M 程度の使用状況だと思います。


f:id:tlync:20111220161147p:image


しかし、このアプリケーションの画面を回転(エミュレータの場合は Ctrl + F11)し、また画面を元に戻しても、使用メモリが増えたままで起動直後の水準に戻らなくなります。やったね、メモリリークの発生です。


f:id:tlync:20111220161146p:image

f:id:tlync:20111220013529p:image

MAT を起動する

実際のケースではメモリリークが起きていると確信を得ている状況ではないかもしれませんが、上記の様にメモリリークの疑いがある状態で、DDMS の 「Dump HPROF file」をクリックし、その時点でのメモリ使用状況をダンプします。


f:id:tlync:20111220013431p:image


尚、Android SDK r14 辺りから、デフォルト設定ではダンブすると MAT で開くのではなく、一度ファイルに保存する様になっている為、DDMS の設定の「HPROF Action」を「Open in Eclipse」に変更しておくと便利です。

MAT 起動後の画面

初回の起動であれば、下記の様な画面が表示されるので「Leak Suspects Report」を選択します。


f:id:tlync:20111220023459p:image


もし、ダイアログが表示されない場合も Overview という画面のフッタにリンクがあり、そこから開けます。

Leak Suspects - リークしてそうなのはこいつらだ

この画面では、要はメモリ使用量が大きいか、または存在するオブジェクト数が多いなどの理由で、MAT がメモリリークの疑いがあると判断した問題の分布がパイチャートと一覧で表示されます。


f:id:tlync:20111220013434p:image


その為、基本的な使い方の流れとして、メモリリークしているオブジェクトが何かの確証が得られてない場合など、まずこの Leak Suspects を参照し、どのオブジェクトがメモリリークを起こしていそうかを、MAT に教えてもらう事からはじめるのが良いと思います。


もし、既にメモリリークしていそうなクラスなどがはっきりしている場合は後述の Dominator Tree からメモリリークを辿るのが早いでしょう。


注意しなければいけない点としては、あくまでメモリリークしていそうなものを表示しているに過ぎない為、最終的にはユーザー自身が意図しないオブジェクトが存在していまっていないかを判断する必要があります。
これは MAT を使う上での重要な事で、メモリリークはシステムによって疑わしき箇所を検出する事は出来ますが、確実にメモリリークを起こしていると判断する事はできません。最終的には MAT のユーザー自身が判断する必要があります。


大事なことなので2回言いました。


話を戻して Leak Suspects の画面に戻ると、どうやら me.tlync.android.example.MemoryAnalyzerDemoActivity の byte[] インスタンスが2つ存在し、それぞれ 3MB 近く消費している事が分かります。


f:id:tlync:20111220013433p:image


今回のケースでは、Activity に someBigObject という static 変数を持っており、その変数に3MB近い byte 配列が代入されている為、3MB を消費している状態自体はメモリリークでも何でもありませんが、それが2つ存在するのは明らかにおかしいと判断する事ができます。


では、何故この様な状態になっているか、を詳細に追求する為に Dominator Tree という、メモリ使用量の大きなオブジェクトの一覧を参照する機能を利用します。

Dominator Tree - 大きいオブジェクトを一覧で見よう

MAT のレポート上部にあるツリー型のアイコンをクリックすると、メモリ使用量(Retained Heap)の多い順にオブジェクトの一覧が表示されます。


f:id:tlync:20111220161148p:image


今回のケースでは分かりやすくトップ2つがまさに問題としてレポートされていたオブジェクトですが、実際のケースでは一覧上には表示されないケースもあります。
その時は、一覧の一番上の という入力欄に正規表現を入力し、対象のクラスを検索する事が出来ます。
※この記事では触れませんが、Dominator Tree に似た Histgram という View を利用すると、xxActivity 毎という様な型毎のオブジェクト数、メモリ使用量を一覧する事ができます。


一覧上でメモリリークしていそうなオブジェクトを見つけたら、ツリーを展開し、最終的にメモリ使用量が大きなプロパティを特定します。


f:id:tlync:20111220031038p:image


上記の例では、下の方の Activity のツリー構成は Activity のインスタンスが byte オブジェクトを持っているだけで特に問題は無い様に見えますが、上の方の Activity のツリー構成からはクラス > インナークラスの参照があるなど、何となく怪しい雰囲気が漂っています。


今回のケースは凄く分かりやすいメモリリークである為、現時点で答えは出ている様なものですが、念の為、Path to GC Root というメニューから、なぜ GC されないのか、つまり誰が参照を保持していてGC されないのかを特定します。

Path to GC Roots - こいつは誰が参照してる?

対象のプロパティを選択し、右クリックし Path to GC Roots > with all referencesを選択します。


f:id:tlync:20111220100753p:image


with all references は、文字通りすべての参照を見る事が出来ますが、SoftReference や WeakReference などを除いて見たい時は、他の exclude ** references を選択してください。


SoftReference と WeakReference ってなんぞ? という方の為に補足すると、SoftReference は OOM になりそうになると解放される参照で、WeakReference は GC の度に優先的に解放される参照です。これらがメモリリークとなる事は実質無い為、常に exclude weak/soft references を選択しても構いません。


Path to GC Roots を実行すると、下記の様な画面が表示され、選択したオブジェクト(今回の場合は byte[])が、どの様なツリーで参照されていて GC されないかが分かります。


f:id:tlync:20111220100755p:image


上記のケースでは、Effective Java で有名な、非 static なインナークラスは、アウタークラスへの暗黙的参照を持つ、という特性からくるメモリリークが発生しています。


その為、今回の対策としては非 static なインナークラスを static に変更し、Context が必要な doSomething には Application Context を渡してやるか、もしくはそもそも innerClass を static に保持する必要が無ければ、変数の方を非 static にする事でメモリリークを解決する事が出来ます。

static class SomeInnerClass {      public void doSomething(Context appContext) {          Toast.makeText(appContext, "Do something", Toast.LENGTH_SHORT).show();      }  }  

まとめ

一見すると難しそうな MAT ですが、基本的な部分を扱うだけならさほど難しくないよ、という事をひとりでも多くの Android プログラマに認識してもらえれば何よりです。


が、MAT はまだまだ機能が豊富で、例えば OQL という n byte 以上のオブジェクトという様な SQL に似た構文でオブジェクトを探せたり、2点間のヒープダンプの差分を出力できたり、と紹介しだすとキリがないほどで、自分自身も知らない機能は沢山ある為、少しずつ使い方を学び、この世の Android アプリからメモリリークを撲滅しましょう。


0 件のコメント:

コメントを投稿