Activity がメモリリークしにくくなってる件

2009年の情報なんですけどね。

Android でメモリリークする典型的なパターンとして上で紹介されているものがあって、日頃はこうならないように気をつけて実装をしているわけです。

また、メモリリークの調査方法もたくさん情報があります。

日頃、Xamarin.Android を触っているので、「Xamarin でも同じようにリークするよね」と思いやってみたところ全然リークしなかったので、もしや Android-Java でもリークしないんじゃ?と考え、試してみたのが以下の内容です。

試した

以下の2つのパターンについて試しました

  1. Avoiding memory leaks の 2番目の例。Activity への強参照を持った Drawable を static なメンバにキープしちゃう件。画面が回転した時に、Activity がリークしてしまう、とされる。
  2. 暇なメモ帳さんの「問題3」+α。非static な Inner クラスが Activity の強参照を持ってる、且つ、このオブジェクトを Activity の static メンバにしちゃう。

結論

から言うと、

1. はリークせず、2. はリークしました。

あれれ?

パターン1のテストコード

ほぼ元コードのコピペだけど、クラスが破棄された(finalize )時にログ吐くようにしています。

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    private static Drawable sBackground;
    
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		Log.d(TAG, "onCreate:" + this.hashCode());
		
        Button button = new Button(this);
        button.setText("Leaks are bad");
        
        if (sBackground == null) {
            sBackground = getResources().getDrawable(R.drawable.ic_launcher);
        }
        button.setBackgroundDrawable(sBackground);
        setContentView(button);
    }
	
    @Override
    protected void onDestroy() {
    	Log.d(TAG, "onDestroy:" + this.hashCode());
    	super.onDestroy();
    }
    
    @Override
    protected void finalize() throws Throwable {
    	Log.d(TAG, "finalize:" + this.hashCode());
    	super.finalize();
    }
}

確認手順

  1. このアプリを実行。Android2.3 のエミュレータ(4.0 の実機でも試した)。
  2. 画面を回転させる(Ctrl+F11)
  3. DDMS から GC を走らせる
  4. LogCat を収集

improve activity leaks 02 improve activity leaks 03

Logcat の出力結果はこう。

03-19 21:28:09.539: D/MainActivity(382): onCreate:1079076320
03-19 21:29:15.979: D/MainActivity(382): onDestroy:1079076320
03-19 21:29:15.989: D/MainActivity(382): onCreate:1079106528 ←横画面のActivity
03-19 21:29:33.939: D/MainActivity(382): finalize:1079076320

ちゃんと GC を走らせた後、 Activity の finalize が呼ばれています。 MAT でも確認したけど、リークは発見できませんでした。

パターン2のテストコード

こんな実装は早々お目にかからないと思うけど、非static な Inner クラスのインスタンスを、Activity の static メンバにしちゃうぞ、と。

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
	private static SomeInnerClass innerClass;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate");
        
        if (innerClass == null) {
            innerClass = new SomeInnerClass();
        }
    }
    
    @Override
    protected void onDestroy() {
    	Log.d(TAG, "onDestroy");
    	super.onDestroy();
    }
    
    @Override
    protected void finalize() throws Throwable {
    	Log.d(TAG, "finalize");
    	super.finalize();
    }

    class SomeInnerClass {
        public void doSomething() { }
    }
}

確認手順

パターン1と同じです。

Logcat の出力結果はこちら。

03-19 21:42:55.289: D/MainActivity(476): onCreate
03-19 21:43:05.369: D/MainActivity(476): onDestroy
03-19 21:43:05.549: D/MainActivity(476): onCreate

ご覧のとおり、finalize が呼ばれない、つまり Activity がリークしています。

考察っぽいの

パターン2 がリークするのは当然と言えます。 Activity への強参照を持ったオブジェクトを、static フィールドで保持し続けてしまうので、Activity が破棄されない。

パターン1 も同じ理屈だと思うのですが(少なくとも冒頭の記事の説明ではそう)。これがリークしないのは、Android SDK が改善された(例えば、今まで Activity の強参照を持ってたのが弱参照に変わった)とか、Dalvik の GC が改善されたとかでしょうか?

まあ4年も経てば常識も変わるということで、Activity に関しては以前ほど神経質にならなくてもいいかもしれませんが、メモリリークの可能性が消えることは有り得ないので、このアンチパターンはこれからも遵守していかないといけませんね。