2012/09/06

Android : Canvas: trying to use a recycled bitmap

Android で、Bitmap をごにゅごにゅしていると、
 java.lang.RuntimeException:"Canvas: trying to use a recycled  bitmap"
ってのに、出くわすことがあります。Web でいろいろ調べてみると、怪しげな解説がかなりあるので、本当のところ何が正しい理解なのかをまとめておきたいと思います。

そもそも、Bitmap#recycle() をやらないといけないのは、主に Android のこの辺の実装がよろしくなくて、recycle() してやらないと、メモリリークになるよ、といわれていることが、大きな要因です。勢い、メモリリーク怖さになんでも recycle() しようとして、件の例外に遭遇することとなる訳です。しかし、どういったケースでそれが必要になるのか、筋の通った記事をあまり見かけません。


まず、誰が  ”trying to use a recycled  bitmap” といっているのかといえば、ログのいう通りCanvas な訳です。自分の書いたソースコードに Canvas が使われてなくても、Canvas は、Android OS の画面描画に使われる訳ですから、画面を描き変える要素があれば、必ず働いてくれています。

よく、
recycle() をしたら、”trying to use a recycled  bitmap” が発生
というように表現されることがありますが、recycle() の中で例外が発生している訳ではありません。recycle() した結果、その後で、recycle された Bitmap が使われる段になって、Canvas が、
既に recycle されてんじゃん
って、文句を言ってる訳です(*1)。

さて、自分で描画しようとしている訳でもないのに、誰が Canvas さんに描いてくださいといっているかといえば、大抵の場合、ImageView がいっている訳です。
Bitmap のインスタンスを ImageView に対して、setImageBitmap() すると、ImageView は、当然 Bitmap を描画しようとします。なので、ImageView の外側で、Bitmap のインスタンスに対して、recycle() すれば、ImageView が描こうとした段階で、件の例外発生と相成る訳です。

ということで、Bitmap#recycle() する前に、インスタンスをセットした ImageView に対して、ImageView#setImageBitmap(null) をしておくことが必要です。これで、件の例外は発生しなくなるでしょう。めでたしめでたし ....

と、そうは問屋が卸さないので Android のおもしろいところで、実は ImageView#setImageDrawable( null ) をするのが正解です。

ImageView#setImageBitmap() のソースコードを見ると

    public void setImageBitmap(Bitmap bm) {
        // if this is used frequently, may handle bitmaps explicitly
        // to reduce the intermediate drawable object
        setImageDrawable(new BitmapDrawable(mContext.getResources(), bm));
    }

となっていて、BitmapDrawable のインスタンスが必ず生成されて、それが setImageDrawable() に渡されています。


    public void setImageDrawable(Drawable drawable) {
        if (mDrawable != drawable) {
            ...(中略)...
            updateDrawable(drawable);
            ...(中略)...
        }
    }
    private void updateDrawable(Drawable d) {
        if (mDrawable != null) {
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
        }
        mDrawable = d;
        ...(中略)...
    }
こうなっているので、setImageDrawable() の引数に null を指定してやると、ImageView のmDrawable に null がセットされ、参照が削除される訳です。

さて、GCがあるのだから、インスタンスへの参照さえなくなれば、問題なくきれいさっぱりメモリ上から消えてくれるはずです。実際大抵の場合そうなってくれます。

ところが、いろいろな事情でインスタンスへの参照が残ってたりすることがままあります。例えば、Activity のインスタンスを Context としていろいろなところに渡していたり、わざわざ、Context として自分自身への参照をメンバ変数に持っていたり、そういった場合、Activity のインスタンスの破棄が、思ったタイミングで起こらないこと請け合いです(*2)。
Bitmap のインスタンスへの参照をメンバ変数として持つ場合も、所有者である(例えば、Activity )のインスタンスの破棄の際にうまく破棄されないことが起こりえます。
いわゆる Finalizer の罠(*3)があるからです。

なので、onDestroy (*4)時などに、もう使わないだろう参照を含む全てのメンバ変数は null にしておくにこしたことはありません。そして、Bitmap に関しては、null にする前にrecycle() しておくのが良いと思われます。そして、もしそれがImageViewで使われているなら、recycle する前に、ImageView に対して setImageDrawable(null) を実行しておくべきです。

と、ガイシュツな結論に達した訳ですが、仕組みを理解した上で、使うことが大事ですね。きっと。

(注)

*1 興味のある人は、Canvas.javaのソースコードリーディングをしましょう。
*2 もちろん、Activityのインスタンスは一定期間破棄せず、再利用するのがAndroid流です。
*3 Finaliserの罠については、たとえば、ここを参照。
*4 onDestroyに関するリファレンスの記述がとても気になる人は、状況が許すのであれば、onPauseあるいはonStopで行うのがいいのかもしれません。