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
実験手順
- MainActivityからBitmapActivityに遷移
- BitmapActivityで戻るボタンを押す
- もう一度MainActivityからBitmapActivityに遷移
- GCを実行する
- DDMSでプロセスを選択
- 「Update Heap」を選択
- 「Cause GC」ボタンを選択
これでGCが実行され不要なメモリは開放される
jhatの起動手順
jhat [プロファイルファイルのパス]
- ターミナルにこんなのが出力される
$ 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.
- ブラウザを起動して「localhost:7000」
実験1 ImageViewを持たないActivity
まずはBitmapもImageViewも持たないActivityで
確認
結論
このケースでは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>
実験3 レイアウトでImageViewを持ち、メンバ変数でもImageViewをもつActivity
BitmapActivityを以下のように変更
実験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; }
実験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を参照しているか確認
まぁだいたい想像通りだけど以下の手順で発見できる
- bitmap.recycle()の箇所でBreakPointを張る
- 止まったタイミングで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:を確認するとこんなのがいる
エラー解決
どうやらBitmapDrawableがbitmapを参照しているので開放する
onPauseに以下を記述
image.setImageDrawable(null);
動作確認
正常に画面遷移するのを確認
戻ってくるのを確認
Homeキーでも正常に動くのを確認
結論
レイアウト指定のImageViewを使ったActivityはrecycleは不要
ソースからBitmap指定のImageViewではrecycleは必須
でもBitmap#recycleだけじゃだめ
ImageView#setImageDrawable(null)もする