Hatena::ブログ(Diary)

hidecheckの日記

2011-06-25

ImageViewとBitmap#recycle覚書

| 02:59

開発してると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

f:id:hidecheck:20110626023918p:image

実験5

MainActivity>BitmapActivity>NextActivity

f:id:hidecheck:20110626023929p:image

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

f:id:hidecheck:20110626024101p:image

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

確認方法

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

確認することは1つ

「BitmapActivityのインスタンスが1つしかないこと」

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

f:id:hidecheck:20110626024355p:image

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」

f:id:hidecheck:20110626024354p:image

実験1 ImageViewを持たないActivity

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

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

このケースでは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>
実験結果

f:id:hidecheck:20110626024836p:image

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

明示的な開放は不要ということになる

つまり、Bitmapの開放はImageViewが勝手にやってくれるということ


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

BitmapActivityを以下のように変更

実験結果

f:id:hidecheck:20110626024923p:image

このケースでも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>
実験結果

f:id:hidecheck:20110626025003p:image

メモリリークキタ━━━━━━(゚∀゚)━━━━━━ !!!!!

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;
	}
結果

f:id:hidecheck:20110626025143p:image

まだいる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に戻ったタイミング
実行結果

f:id:hidecheck:20110626133901p:image

エラー発生!

こんなエラーが吐かれる

  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

f:id:hidecheck:20110626025617p:image

発見!

犯人はBitmapStateでした

 エラー解決

どうやらBitmapDrawableがbitmapを参照しているので開放する

onPauseに以下を記述

image.setImageDrawable(null);

動作確認

正常に画面遷移するのを確認

戻ってくるのを確認

Homeキーでも正常に動くのを確認

結論

レイアウト指定のImageViewを使ったActivityはrecycleは不要

ソースからBitmap指定のImageViewではrecycleは必須

でもBitmap#recycleだけじゃだめ

ImageView#setImageDrawable(null)もする

shisei_ssishisei_ssi 2011/06/26 14:59 結論の一番最後はあっていますが、過程がいくつか間違っているのでコメントいたします。

>java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@44c03b58
これは「Bitmapを描画しようとしたけどrecycleで解放済みだからできない」という意味です。

Bitmapは明示的なrecycle()は別に必要ではなく、GCで回収されればBitmap#finalize()でネイティブ側のメモリも解放されます。
というか一度recycle()してしまうとそのBitmapは二度と再利用できなくなるため、メンバの参照を捨てるのと実質大差ありません。
recycle()が有用なのはGCを起こさずにネイティブ側の(大きな)メモリを解放できる点です。

なので実験4に onPause() での image.setImageDrawable(null); を追加するだけで、GCを起こせばBitmapも解放されているはずです。

今回BitmapActivityがImageView#setImageBitmapでリークした問題は、Bitmapとは関係ありません。
リークの理由は、
・ImageViewは、ContextとしてActivityの参照をメンバに持っている
・ImageViewのメンバのDrawableには、Drawable#setCallbackでImageViewの参照が設定される
・ImageViewのメンバのDrawableは、View#scheduleDrawableを使ってメインスレッドのHandlerにメッセージと共にpostされる。View#unscheduleDrawableが呼ばれれば未処理のメッセージは解除される。
・ImageView#setImageDrawableやsetImageBitmapを呼べば、以前のDrawableに対してView#unscheduleDrawableは実行される
・手順1と手順3では別々のBitmapActivityのインスタンスが起動しているため、そのメンバのimageのインスタンスも異なる。つまり実験4では手順1のBitmapActivityのimageのDrawableが解除される機会がない。
ということになると思います。

hidecheckhidecheck 2011/06/27 00:05 とても丁寧な説明ありがとうございます。参考になります。ありがたやありがたや

以下、確認しました。
>これは「Bitmapを描画しようとしたけどrecycleで解放済みだからできない」という意味です。
これはその通りですね。後で修正しておきます。

>なので実験4に onPause() での image.setImageDrawable(null); を追加するだけで、GCを起こせばBitmapも解放されているはずです。
次の記事で書かせていただきました。