Android のメモリ管理 #2 Allocation Tracker でメモリに割り当てられたオブジェクトを調べる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Allocation Tracker はある時間とある時間の間でメモリにどのような種類のオブジェクトがアロケート(割り当て)されたかを調べることができるツールです。Android SDKに含まれている機能なのでAndroidの開発環境が最低限整っていれば誰でも使用することができます。
前回のブログでは概要のみでしたので、今回はこのツールの機能と活用方法について具体的に見ていこうと思います。

アロケートを確認してみる

説明が多くても分かりづらいと思うので、まずはアロケートを確認してみましょう。 例として、以下のようなカスタムViewを作りました。

CustomView.java

package jp.classmethod.android.sample.sampleproject;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CustomView extends View {

	private static final String TAG = CustomView.class.getSimpleName();

	public CustomView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	public CustomView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public CustomView(Context context) {
		super(context);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		Paint paint = new Paint();
		paint.setColor(Color.MAGENTA);
		RectF rect = new RectF(0, 0, getWidth(), getHeight());
		canvas.drawRoundRect(rect, 10f, 10f, paint);
		Log.d(TAG, "onDraw");
	}

}

MainActivity.java

package jp.classmethod.android.sample.sampleproject;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;

public class MainActivity extends Activity {

	private static final String TAG = MainActivity.class.getSimpleName();

	private CustomView view;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		view = new CustomView(getApplicationContext());
		setContentView(view);
	}
}

実行結果

マゼンタの角丸付きの Rect が描画されるだけの View です。 この Activity を生成するときにアロケートされるオブジェクトを見ていきたいと思います。 手順としては下記のとおりです。

  1. 「Start Tracking」ボタンを押す
  2. アロケートを確認したい処理を行わせる
  3. 「Get Allocations」ボタンを押す

この手順で、「Start Tracking」を押したタイミングから「Get Allocations」を押すタイミングまでにアロケートされたオブジェクトを確認することができます。 実際に確認してみた結果は下図のとおりです。

見覚えのないオブジェクトが並んでいますが、これは CustomView 以外のクラスでアロケートが発生したオブジェクトです。ソートすると CustomView の処理内でアロケートが発生したオブジェクトが分かります。

onDraw() で生成した RectF や Paint オブジェクトがあるのがわかると思います。 onDraw() はまだ一度しか呼ばれていないのでオブジェクトはひとつずつしかありませんが、描画を何回も呼び出すような処理に書き換えてみます。

MainActivity.java

package jp.classmethod.android.sample.sampleproject;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;

public class MainActivity extends Activity {

	private static final String TAG = MainActivity.class.getSimpleName();

	private CustomView view;
	private Handler handler;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		view = new CustomView(getApplicationContext());
		setContentView(view);
		handler = new Handler();
		handler.postDelayed(runnable, 500);
	}
	
	Runnable runnable = new Runnable() {
		@Override
		public void run() {
			view.invalidate();
			handler.postDelayed(runnable, 500);
		}
	};
}

先ほどと同じ手順で調べた結果が以下のとおりです。

ぎゃあ。Paint オブジェクトが大量にアロケートされています(ちなみにスクロールすると RectF も大量にあります)。これでは描画が発生するたびに同じオブジェクトが作られ続けてしまい、メモリリークの原因になってしまいます。
ということで、 CustomView を以下のように修正します。

package jp.classmethod.android.sample.sampleproject;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CustomView extends View {

	private static final String TAG = CustomView.class.getSimpleName();
	
	private Paint paint;
	private RectF rect;

	public CustomView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initialize();
	}

	public CustomView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initialize();
	}

	public CustomView(Context context) {
		super(context);
		initialize();
	}
	
	private void initialize() {
		paint = new Paint();
		rect = new RectF();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		paint.setColor(Color.MAGENTA);
		rect.right = getWidth();
		rect.bottom = getHeight();
		canvas.drawRoundRect(rect, 10f, 10f, paint);
		Log.d(TAG, "onDraw");
	}

}

Paint オブジェクトや RectF はひとつのオブジェクトで問題ないので、 onDraw() の処理でインスタンスを生成しないようにしました。MainActivity はそのままにして調べた結果が以下のとおりです。

Paint オブジェクトと RectF オブジェクトがひとつずつしかアロケートされていないことが確認できます。無事にメモリリークの原因を解消することができました!このようにして、ある処理の間に無駄なアロケートがないか調べることができます。

短命なオブジェクトの生成を抑えよう

Android ではメモリ不足になると GC(ガベージコレクション) が起動され、必要のないオブジェクトをメモリから開放してくれるようになってはいます。しかし onDraw() や onLayout() などのような頻繁に実行される処理の中でアロケートが行われると、GCも頻繁に実行されることになり、著しくパフォーマンスに影響が出てしまいます(例えば ListView のスクロールがカクカクしてしまうなど)。そのため、短命なオブジェクトの生成は可能な限り最小限に抑えるようにすべきです。この点を注視して実装するように常に心がけていれば、あとは自然とクセがついてきます。Allocation Tracker を活用しながら "メモリ節約家" になりましょう!

まとめ

今回はありがちな例を通して、 Allocation Tracker を使ったメモリリークの原因の調査方法について解説しました。 Allocation Tracker のメリットはどのオブジェクトがアロケートされたか明確に分かる点ではありますが、このメリットは どのあたりの処理でメモリリークが発生しているか絞り込めているときのみ威力を発揮します。 何故ならば、実際のアプリの実装ソースはもっと複雑であり、オブジェクトの生成によるアロケートも数多く発生するため、 Allocation Tracker を使ってすべてのメソッドのアロケートを調べるということは非常に非効率であり、多くの場合が無意味な結果になるからです。 Allocation Tracker は、メモリリークが発生している原因を生み出しているクラスを絞り込んだうえで「ではどのオブジェクトが原因なのか」調べるときに使うべきです。
その「メモリリークの絞り込み」については Heap dump を使うことが有効的です。ということで、次回は Heap dump について解説したいと思います。