sandbox

Scala, Android, Architecture, Management, Service Design あたりを主戦場としております

ADT r20 の新機能について LT したよ

デモメインの発表用資料なので、あまりまとまっていないという噂もある。

やっぱり、個人的には Java ファイルの保存時の Lint の自動実行が一番嬉しいかな。
逆に言うと他はわりとどうでもいい。

Jenkins と始める Android プロジェクトでの CI - Ant 基本編

f:id:tlync:20120326011643p:image

Goal

Jenkins で達成出来る事は沢山ありますが、この記事では複雑な設定を伴わないで実現可能な、apk の自動生成、テストの自動実行までを対象とします。


またビルドツールも Ant, Maven, Gradle, Ivy 等がありますが、標準でもサポートされており、最小構成な Ant を選択しています。
※ Jenkins でどこまで自動化したいかによりますが、様々なタスクを実行しようと思うと豊富な Plugin を持つ Maven が便利ではあるので、それはまた別途。

ant でビルド出来る様にする

Jenkins で CI するには、まずはプロジェクトをコマンドでビルド出来る必要があります。
Eclipse で作成したプロジェクトでは、そのままではビルドする事が出来ない為、後から Android SDK に含まれるコマンドを利用して Ant 用の build.xml を生成します。

android update project -p {プロジェクトのパス} -n {プロジェクト名}


もし、テストプロジェクトやライブラリプロジェクトを利用しているのであれば、各プロジェクトでも下記のコマンドを実行します。


テストプロジェクト

android update test-project -p {プロジェクトのパス} -m {テスト対象のプロジェクトのパス}


ライブラリプロジェクト
Android プロジェクトの方で -l オプションを利用し 、ライブラリプロジェクトを指定する必要がある

android update lib-project -p {プロジェクトのパス}


ここまで行うと ant debug, ant release などといったコマンドで debug 用の apk、release 用の署名されていない apk を生成出来る様になります。
署名済みの apk を自動生成するには keystore のディレクトリパスを登録する必要がある為、ant.properties というファイルを、 メインプロジェクトのルートに作成します。

key.store=../path/to/release.keystore
key.alias=test
# パスワードも登録すると完全に自動化できる
#key.store.password=password
#key.alias.password=password

パスワードを登録するのがセキュリティ的に問題があれば、Jenkins の設定で指定する事も出来ます。

コマンドを試す

メインプロジェクトでは、ant release とすると署名済みの apk が、プロジェクトの bin 以下に生成されます。

ant clean release


テストプロジェクトでは、テストプロジェクト以下で下記のコマンドでテストを実行できます。

ant clean emma debug install test


デバイスを指定する場合はこんな感じ

ant -Dadb.device.arg="-s emulator-5554" clean emma debug install test

emma はカバレッジの生成なのでなくてもいいです。

Android Emulator Plugin のインストール

Jenkins には、Jenkins サーバー上でエミュレータを自動で起動する事が出来るすばらしいプラグインが提供されています。
Manage Jenkins > Manage Plugin > Available にある、Android Emulator Plugin にチェックしてインストールします。

Jenkins の Job を作成する

1. New Job を選択し、Build a free style project を選択します。

f:id:tlync:20120326002418p:image

2. Source Code Management では、SCM に応じた設定をするだけで、特別な設定は必要ありません。メインプロジェクト等が含まれるルートディレクトリを指定します。


3. Build Environment で、テストを実行する対象の emulator を指定します。存在する emulator を指定してもいいですし、パラメータを指定し、新規に作成する事も出来ます。また、このプラグインは Android SDK が入っていなければ自動でダウンロードしてインストールしてくれるという優れもの。


4. Buildでは、メインプロジェクト用、テストプロジェクト用の ant ターゲットをそれぞれ指定します。


f:id:tlync:20120326005224p:image


もし、まだ Jenkins の設定で ANT_HOME を指定していない場合は、その設定をする必要があるかもしれません。


5. Post Build Action で、apk を Jenkins の成果物として扱う為、apk のパスを指定します。


f:id:tlync:20120326005742p:image


以上の設定で、Android プロジェクトのテストの実行、apk 生成までが自動化できているはずです。


f:id:tlync:20120326005849p:image


上記 apk の QR コードをどこかの wiki に貼っておくなどすれば、常に最新の apk をインストールする事ができます。
あとはこの Job を svn, git のコミットフックにしかければ完全に自動化する事ができます。

まとめ

紹介してきた一連の流れは最小限の設定に過ぎない為、テストの実行しているのにレポートがダッシュボードに出力されていないなど、残念なところが多々あります。
maven android plugin を使うとおのずと解決される問題もありますし、ant で頑張って解決する方法もあるので、それらについてはおいおい書いていきたいと思います。

Android Bazaar Conference 2012 Spring に参加してきた

当日は、前日まで忙しくしていた事もあり登壇予定 LT の準備に終われ、直前まで資料作りに励み、セッションは全然参加できなかったという本末転倒感のある結果に。
なので、セッションの感想は全然なしです。

LT で発表した

「Droid 君と Jenkins 氏の CI 3分クッキング」というタイトルで発表しました。


感覚的ではあるけども Web アプリケーションの領域と比較して、Android 周りでは CI がそれ程認知、普及されていない気がしたので、わりと簡単だし今日から少しでも自動化しよう、というのが主旨でこのテーマを選びました。



…が、結果としては、発表時のディスプレイ周りの詰めが甘く、デモもままならず散々な結果に orz
発表スキルはまだ底辺なので、課題も見えたし今後の糧として前向きに捉える事にします。
え、泣いてなんかいません。


尚、スライドに内容があまりないので補完エントリを書きました。

http://d.hatena.ne.jp/tlync/20120326/1332691894

懇親会

開始時より、名刺交換のオンパレードでなんか前職の B2B 系のイベントな雰囲気に若干萎えつつも、結果的に色々な方と話せて楽しかったです。わりとお金の匂いのする懇親会ではあるので、技術について語りたい開発者同士の懇親会は別枠でやった方が楽しいだろうなぁ…とは思いました。


良く知らないですが、コアな Android クラスタの人達が集る裏懇親会と言うものがあったらしく、そっちに興味はあったけど 5000円払っちゃたし、なんか怖いので普通にオモテに参加しました。


他の会社の開発者の方と合同勉強会などをしたいねなんて話しも出来たので、なにか合致するテーマがあれば実現に向けて動きたいところ。

色々課題はありますが、来年も何かしら発表側で参加出来る様に頑張りたい。
主催、運営、講師の方々、お疲れ様でした & ありがとうございました。

ファイル保存後に iPhone Simulator 内の Safari をオートリロードする elisp & Apple Script

iPhone Simualtor でも、ファイル保存後の auto-refresh がしたかったので。

リロードを実行する Apple Script

iPhone Simulator & Mobile Safari が起動している前提で、アクティブなタブをリロードする。

tell application "System Events"
	tell process "iPhone Simulator"
		click button "Reload" of window 1
	end tell
end tell

elisp 上でワンライナーでコーディングしてしまうのでファイル保存は不要

ラッパー関数 & auto-save-hook

Emacs 上で扱う為に、ラッパー関数を定義して、after-save-hook に仕込む。

web-reload-iphonesimulator

上記の Apple Script をワンライナー化して、関数として定義。

(defun web-reload-iphonesimulator ()
   "Reload a page on iPhone Simulator. Run process associated to the *Messages* buffer"
   (interactive)
   (start-process-shell-command
       "iphonesimulator-process"
       "*Messages*"
       "osascript -e 'tell application \"System Events\"' -e 'tell process \"iPhone Simulator\"' -e 'click button \"Reload\" of window 1' -e 'end tell' -e 'end tell'"))
after-save-hook の設定例:

対象のファイルを保存するとすべてのブラウザで更新を走らせるという富豪仕様 ;p

(setq web-enable-autoreload t)
(setq web-autoreload-filetypes '("css" "js" "php" "html" "htm" "tpl" "jade" "haml" "mustache"))
(defun web-autoreload-browsers()
  (interactive)
  (if (and
       (equal web-enable-autoreload t)
       (string-match
        (concat "\\.\\("
                (mapconcat 'identity web-autoreload-filetypes "\\|")
                "\\)$")
        buffer-file-name))
      (web-reload-browsers)))

;; for Firefox
;; required mozrepl
(defun web-reload-firefox ()
  (interactive)
  (moz-send-line "content.location.reload()"))

;; for Chrome
(defun web-reload-chrome ()
   "Reload a page on Chrome. Run process associated to the *Messages* buffer"
   (interactive)
   (start-process-shell-command
       "chrome-process"
       "*Messages*"
       "osascript -e 'tell application \"Google Chrome\" to reload active tab of window 1'"))

;; for iPhone Simulator
(defun web-reload-iphonesimulator ()
   "Reload a page on iPhone Simulator. Run process associated to the *Messages* buffer"
   (interactive)
   (start-process-shell-command
       "iphonesimulator-process"
       "*Messages*"
       "osascript -e 'tell application \"System Events\"' -e 'tell process \"iPhone Simulator\"' -e 'click button \"Reload\" of window 1' -e 'end tell' -e 'end tell'"))

;; Reload all browsers
(defun web-reload-browsers ()
  (interactive)
  (web-reload-iphonesimulator)
  (web-reload-firefox)
  (web-reload-chrome))

Proguard を利用してリリース apk からログ出力をストリップする方法 & ベンチマークをとってみた

はじめに

皆さんは Android アプリにログ出力をどの程度仕込んでいるでしょうか。
適切にログ出力が施されたアプリは、問題追跡がしやすいですし、あの糞重いデバッガを起動しなくて済む事が多いです。


個人的には、verbose or debug レベルでは、ふんだんにログ出力を仕込みたいと考えていますが、あまりログを仕込みたがらない人がいるのも事実です。
ログ出力普及委員会(会員1名)の市場調査結果では、その理由として大きく下記の2つがある様でした。

  • 標準の Android Log の仕様上、リリースされた apk でもログがすべて参照できてしまう = 気軽に内部データのログ出力を書けない
  • ログ出力によるパフォーマンスの低下の懸念

これらの問題へのソリューションとしてはいくつか方法がありますが、この記事では proguard を利用した下記の方法を検証してみました。

  • ログが見えてしまう → proguard の設定で、リリース apk ではログ出力のメソッドコール自体を消す
  • パフォーマンス低下の懸念 → 上記の様なバイトコードレベルでのメソッド除去を行えば、理論的にはログ出力がパフォーマンスに与える影響はないはず。これをベンチマークを測定し検証してみる。


つまり、これらをはっきりさせ、みんなにログをもっと書いてもらおうというのがこの記事の主旨です。

検証コード

検証用のコードとして下記の様なものを準備しました。
処理内容は、テキストビューに 10000 回のログ出力にかかった時間を出力するだけのものです。


LogBenchmark.java

public class LogBenchmarkActivity extends Activity {

    private static final String TAG = LogBenchmarkActivity.class.getSimpleName();
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        for (int i=0; i<10; i++) {
            performBenchmark();
        }
    }

    protected void performBenchmark() {
        TextView empty = (TextView) findViewById(R.id.empty);
        TextView standard = (TextView) findViewById(R.id.normal);
        TextView wrapper = (TextView) findViewById(R.id.wrapper);

        // パフォーマンス測定用 基準値
        long s0 = System.currentTimeMillis();
        for (int i=0; i<10000; i++) {
            // 空のループ
        }
        long e0 = System.currentTimeMillis();
        empty.setText(empty.getText() + LINE_SEPARATOR + (e0 - s0 + " ms"));

        // Android 標準の Log クラスを使用
        long s1 = System.currentTimeMillis();
        for (int i=0; i<10000; i++) {
            Log.d(TAG, "i: " + i);
        }
        long e1 = System.currentTimeMillis();
        standard.setText(standard.getText() + LINE_SEPARATOR + (e1 - s1 + " ms"));

        // 独自の Logger クラスを利用 ※詳細は後述
        long s2 = System.currentTimeMillis();
        for (int i=0; i<10000; i++) {
            Logger.d(TAG, "i: %s", i);
        }
        long e2 = System.currentTimeMillis();
        wrapper.setText(wrapper.getText() + LINE_SEPARATOR + (e2 - s2 + " ms"));
    }

}


次に、proguard の設定で、各 Log クラスのメソッド呼び出しをすべて除去します。
※-assumenosideeffects は、「返値が利用されていないと判断できる」という意味です。


proguard.cfg

# 標準で設定されるものは省略

# strip logging
-assumenosideeffects class android.util.Log {
    <methods>;
}

-assumenosideeffects class me.tlync.android.example.Logger {
    <methods>;
}

上記のソースコードと設定では、Android 標準の Log クラスの他に、自作の Logger クラスを定義、それに対する設定を追加しています。


これは、ログストリップ時の更なるパフォーマンス向上(と利便性)の為に定義しているもので、標準の Log クラスを使った事がある方はご存知の通り、引数に可変長引数や配列を取る様になっていない為、上記コードの様な Log.d(TAG, "i: " + i) が呼ばれた時点で、第2引数の文字列構築が発生し、このコストがログストリップを施したリリース apk 上でも残ってしまいます。
その為、対策として、メソッドコールの時点で文字列構築が行われない様に、下記の様な可変長引数を用いたラッパー関数を定義しています。

public static int d(String tag, String format, Object... args) {
    return Log.d(tag, format(format, args));
}

プロジェクトのソースコードと、Logger の実装例は下記の URL を参照してください。


Projecthttps://github.com/tlync/android-sandbox/tree/master/LogBenchmark
Loggerhttps://gist.github.com/1868304


Logger に関しては、独自のものでなく SL4J for android などを使うのもありです。

検証結果

1. ログストリップが出来ているか

ログが見えてしまう事が問題でしたが、上記の proguard の設定を施した apk では想定通りログ出力されていません。

念の為、バイトコードも確認してみましたが、ちゃんとバイトコードレベルで消えてます。

$ diff -u bin/proguard/nonstripped-dump.txt bin/proguard/stripped-dump.txt
--- bin/proguard/nonstripped-dump.txt	2012-02-22 14:36:46.000000000 +0900
+++ bin/proguard/stripped-dump.txt	2012-02-22 14:37:58.000000000 +0900

@@ -31,12 +27,10 @@
   - Fieldref [me/tlync/android/example/LogBenchmarkActivity.b Ljava/lang/String;]
   - Methodref [android/app/Activity.<init> ()V]
   - Methodref [android/app/Activity.onCreate (Landroid/os/Bundle;)V]
-  - Methodref [android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I] # ちゃんと消せてる

# 略

         - Fieldref [me/tlync/android/example/LogBenchmarkActivity.a Ljava/lang/String;]
         - Class [java/lang/StringBuilder]
         - String [i: ]
         - Methodref [java/lang/StringBuilder.<init> (Ljava/lang/String;)V]
         - Methodref [java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;]
-      [127] invokevirtual #35
-        - Methodref [java/lang/StringBuilder.toString ()Ljava/lang/String;]
-      [130] invokestatic #23
-        - Methodref [android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I]  # ちゃんと消せてる
-      [133] pop

# 略

         - Methodref [java/lang/System.currentTimeMillis ()J]
-      [148] lstore v11
2. パフォーマンスに影響はないか

ではパフォーマンスはどうだったでしょうか。
いずれも Proguard をかけた release apk での結果です。


ログストリップ 設定なし
f:id:tlync:20120222021002p:image


ログストリップ 設定あり
f:id:tlync:20120222021003p:image


結果としては、

  • 設定なし … String#format を実行コストが高い為、wrapper 版のログ出力が、標準の Log 出力より遅い
  • 設定あり … 結果が逆転し、文字列構築のコストが浮いた分 wrapper 版のログ出力の方が早い

となっています。


概ね予想通りですが、理論上は wrapper 版は empty 版(空ループ)と同じになるはずなので、若干腑に落ちません。
そこでバイトコードを見てみると、犯人が見つかりました。

        - Fieldref [me/tlync/android/example/LogBenchmarkActivity.a Ljava/lang/String;]
      [201] pop
      [202] iload v15
      [204] invokestatic #22
        - Methodref [java/lang/Integer.valueOf (I)Ljava/lang/Integer;] # Autoboxing!!!!!
      [207] pop
      [208] iinc v15, 1
      [211] iload v15
      [213] sipush 10000
      [216] ificmplt -18 (target=198)
      [219] invokestatic #30
        - Methodref [java/lang/System.currentTimeMillis ()J]

そう、オートボクシングの存在をすっかり忘れてました。


試しにオートボクシングが発生しない様にすると、empty と同様の 1ms などをマークする様になりました。
proguard の設定を詰めれば Integer#valueOf などのコールすらも除去できますが、影響範囲も大きい為、現段階では許容しています。


結果として、パフォーマンス的にも proguard を利用した方法だと、実行速度に殆ど影響がない事が分かりました。
標準のロガーか、可変長引数でラップするロガーを使用するかに関しては、正直そこまで差は無い為、パフォーマンス要件と好みに応じて使い分ければいいと思います。

まとめ


proguard を使えば、ログ出力のメソッドコールそのものを消せるので、内部データも安心して出力でき、パフォーマンス的にも安心


リリース apk でも全部ストリップするのが問題であれば、verbose だけストリップするというのもありでしょう。
いずれにせよ、これで安心してログが書けますね。

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

はじめに

Android プログラマのみなさん、こんにちは。
今日も元気に Out Of Memory してますか?


ということで、この記事では日々 OOM に悩まされる Memory 的な意味で富豪的な Android プログラマの為に、Eclipse Memory Analyzer Tool、通称 MAT の基本的な使い方を紹介します。


尚、この記事は [twitter:@youten] さんが企画された Android Advent Calendar 12/20 の記事ですが、内容的には比較的オモテなものになっています。

対象読者

  • 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 アプリからメモリリークを撲滅しましょう。


明日の Android Advent Calendar は、表 [twitter:@joinus_jp] さん、裏 [twitter:@furusin_oriver] さんです!

*1:Histgram や OQL でも