ImageViewとBitmap#recycle覚書

開発してるとActivityにBitmapを持たせたいことってよくある
でもメンバで持ってると自前で解放しなくてはならない。
Bitmapのメモリ管理はネイティブ側で管理されてるので明示的に開放する必要がある。
マジで?って思ったので実験してみた

実験内容

以下のパターンでBitmapActivityがどのように変化するかを確認

  • 実験1 ImageViewを持たないActivity
  • 実験2 レイアウトでImageViewを持ったActivity
  • 実験3 レイアウトでImageViewを持ち、メンバ変数でもImageViewをもつActivity
  • 実験4 ImageViewを持ち、メンバ変数でBitmapをもつActivity
  • 実験5 Bitmap#recycleの正しい使い方

使うアプリ

こんな感じのアプリ
実験2〜4
MainActivity>BitmapActivity>(戻るキーで)MainActivity>BitmapActivity

実験5
MainActivity>BitmapActivity>NextActivity

実験手順
  1. MainActivityからBitmapActivityに遷移
  2. BitmapActivityで戻るボタンを押す
  3. もう一度MainActivityからBitmapActivityに遷移
  4. GCを実行する
    1. DDMSでプロセスを選択
    2. 「Update Heap」を選択
    3. 「Cause GC」ボタンを選択


これでGCが実行され不要なメモリは開放される

確認方法

確認方法はandroidではおなじみのjhatを使ってメモリリークを調査する

確認することは1つ
「BitmapActivityのインスタンスが1つしかないこと」

jhatの起動手順
  1. 「Dump HPROF file」を選択(「Update Heap」のとなりにある)
  2. Eclipseのタイトルバーにプロファイルファイルのパスが表示されるのを確認
  3. jhatを起動する

jhat [プロファイルファイルのパス]
  1. ターミナルにこんなのが出力される
$ jhat /var/folders/D3/D34bfnJxFxabnpZY-1-X6++++TI/-Tmp-/android8074444770690181288.hprof 
Reading from /var/folders/D3/D34bfnJxFxabnpZY-1-X6++++TI/-Tmp-/android8074444770690181288.hprof...
Dump file created Mon Apr 25 00:27:02 JST 2011
WARNING: Stack trace not found for serial # -1
    ・
    ・
    ・
Chasing references, expect 8 dots........
Eliminating duplicate references........
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
  1. ブラウザを起動して「localhost:7000」


実験1 ImageViewを持たないActivity

まずはBitmapもImageViewも持たないActivityで

確認
  1. jhatを起動する
  2. Package com.hidecheck.recycleで「class com.hidecheck.recycle.BitmapActivity [0x44e70258]」を選択
  3. References to this object:でBitmapActivityが1つしかないことを確認する
結論

このケースではActivityはただしく解放されている

ソース

MainActivity.java

public class MainActivity extends Activity {
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
	public void onNextButtonClick(View v){
		Intent intent = new Intent(this, BitmapActivity.class);
		startActivity(intent);
	}
    
}

BitmapActivity.java

public class BitmapActivity extends Activity {
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.recycle);
    }
}

recycle.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<TextView
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:text="BitmapActivity" />
 </LinearLayout>

実験2 ImageViewを持ったActivity

レイアウトの変更

recycle.xmlにImagViewを追加

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<TextView
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:text="BitmapActivity" />
	<ImageView
		android:src="@drawable/python_logo"
		android:id="@+id/imageView1"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"></ImageView>
</LinearLayout>
実験結果


このケースでもActivityは1つしかないことを確認できる
明示的な開放は不要ということになる
つまり、Bitmapの開放はImageViewが勝手にやってくれるということ

実験3 レイアウトでImageViewを持ち、メンバ変数でもImageViewをもつActivity

BitmapActivityを以下のように変更

実験結果


このケースでもActivityは1つしかないことを確認できる

実験4 ImageViewを持ち、メンバ変数でBitmapをもつActivity

ImageViewの表示画像をプログラムから指定する
BitmapActivityとrecycle.xmlを以下のように変更

public class BitmapActivity extends Activity {
    private ImageView image;
    private Bitmap bitmap;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.recycle);
        image = (ImageView)findViewById(R.id.imageView1);
    }

    @Override
    protected void onResume() {
    	super.onResume();
    	bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.python_logo);
    	image.setImageBitmap(bitmap);
    }
}
	<ImageView
		android:id="@+id/imageView1"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"></ImageView>
実験結果


メモリリークキタ━━━━━━(゚∀゚)━━━━━━ !!!!!
BitmapActivityが2つある!
これでBitmapは明示的に解放しなくてはいけないってことがはっきりした。

実験5 メモリリークを突き止める

Bitmap#recycleを使って明示的に解放する
ソースを以下のように変更

public class BitmapActivity extends Activity {
    private ImageView image;
    private Bitmap bitmap;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.recycle);
        image = (ImageView)findViewById(R.id.imageView1);
    }

    @Override
    protected void onResume() {
    	super.onResume();
    	bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.python_logo);
    	image.setImageBitmap(bitmap);
    }
    
  @Override
	protected void onPause() {
		super.onPause();
		bitmap.recycle();
		bitmap = null;
	}
結果


まだいるorz
どうやらGCが実行されていない様子。
ていうかrecycleが実行されていないっぽい。
GC実行されるとこんなログがでるが、onPauseの時にはでない

GC freed 113 objects / 4968 bytes in 63ms

実験5-2

これはrecycleを使っても、即実行されるわけではないことかな??
いろいろやってみたがどのタイミングで実行されるかはまちまちだった。
必ず起きるわけではないようだが、以下のタイミングで発生した

  • Homeキーを押したタイミング
  • Homeキーを押して再表示したタイミング
  • NextActivityを追加してBitmapActivity>NextActivityに遷移したタイミング
  • NextActivityを追加してBitmapActivity>NextActivity>BitmapActivityに戻ったタイミング
実行結果


エラー発生!
こんなエラーが吐かれる

  190          AndroidRuntime  E  Uncaught handler: thread main exiting due to uncaught exception
  190          AndroidRuntime  E  java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@44c03b58
  190          AndroidRuntime  E  	at android.graphics.Canvas.throwIfRecycled(Canvas.java:955)
  190          AndroidRuntime  E  	at android.graphics.Canvas.drawBitmap(Canvas.java:1044)
  190          AndroidRuntime  E  	at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:323)
  190          AndroidRuntime  E  	at android.widget.ImageView.onDraw(ImageView.java:845)
  190          AndroidRuntime  E  	at android.view.View.draw(View.java:6535)
  190          AndroidRuntime  E  	at android.view.ViewGroup.drawChild(ViewGroup.java:1531)
  190          AndroidRuntime  E  	at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1258)

java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@44c03b58
これは"recycleしようとしたんだけど他でまだ参照してるから出来なかったです"という意味

調査開始

どのオブジェクトがBitmapを参照しているか確認
まぁだいたい想像通りだけど以下の手順で発見できる

  1. bitmap.recycle()の箇所でBreakPointを張る
  2. 止まったタイミングでjhat(このときはGCはかけない)

jhatより以下の手順で確認

  • Package com.hidecheck.recycleでBitmapActivityを選択
  • References to this object:の「com.hidecheck.recycle.BitmapActivity@0x44ebc490 (157 bytes) : ??」を選択
  • Instance data members:の「bitmap (L) : android.graphics.Bitmap@0x44ebbc90 (30 bytes) 」を選択
  • References to this object:を確認するとこんなのがいる
    • android.graphics.drawable.BitmapDrawable@0x44e8f148 (56 bytes) : field mBitmap
    • com.hidecheck.recycle.BitmapActivity@0x44ebc490 (157 bytes) : field bitmap
    • android.graphics.drawable.BitmapDrawable$BitmapState@0x44e91f88 (36 bytes) : field mBitmap


発見!
犯人はBitmapStateでした

エラー解決

どうやらBitmapDrawableがbitmapを参照しているので開放する
onPauseに以下を記述

image.setImageDrawable(null);

動作確認

正常に画面遷移するのを確認
戻ってくるのを確認
Homeキーでも正常に動くのを確認

結論

レイアウト指定のImageViewを使ったActivityはrecycleは不要
ソースからBitmap指定のImageViewではrecycleは必須
でもBitmap#recycleだけじゃだめ
ImageView#setImageDrawable(null)もする