ImageSwitcher でのメモリ管理| memory leak

スポンサーリンク
スポンサーリンク

ImageSwitcher

Androidでスライドショーを作るにはAnimationも扱えるImageSwitcherを用います。

昨今のスマートフォンやTV端末(fireTVなど)の高解像度化で使用する画像サイズも大きくなってきました。

しかし、、、

ImageSwitcherは大きな解像度の画像を扱ったり、長時間自動でスライドショーを表示させるような場合に、必ずと言ってよいほどメモリリークを起こし、頻繁にアプリが落ちてしまいます。

上記のグラフ画像はスライドショーアプリのメモリ使用状況(青色)です。何も処理をしない場合、上側のように階段状にメモリは増え続けます。何度かAndroid側でメモリ開放を行ってはいますが、結局、×印の地点でアプリは強制的にシャットダウンされました。では、下側のように直線状になるようメモリ管理をしっかり行うにはどうしたらよいでしょうか。

問題点は?

ごく一般的なImageSwitcherのコードは以下のようになります。

bitmap = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]]);
bitmapDrawable = new BitmapDrawable(getResources() , bitmap);
mImageSwitcher.setImageDrawable(bitmapDrawable);

明示的にbitmap化してください。リソースのままではうまくメモリ管理をうまく行えません。 原因は画像データのゴミが蓄積されるためです。記述上ではbitmapに新しく値を入れてはいるのですが、実際はそううまく入っておらず、余った部分があったりで、ゴミが溜まっていきます。

とりあえず、ゴミ処理をする

mImageSwitcherにsetImageDrawable(………)する前に、mImageSwitcher.setImageDrawable(null);を入れる方法をよく見かけますが、ほとんど意味はありません。 実際にAndroidProfilerで確認すると変化がほとんどないことが確認できるでしょう。 そこで以下のように記述します。 ゴミ処理を加えたコード

if(bitmap!=null){
   bitmap.recycle();
   bitmap = null;
}
bitmap = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]);
bitmapDrawable = new BitmapDrawable(getResources() , bitmap);
mImageSwitcher.setImageDrawable(bitmapDrawable);

この記述でメモリリークは回避されます。ImageSwitcherに限らず、bitmapを扱う際はrecycleは必ず必要といってよいでしょう。

outAnimationに対応させる

ただし、この記述の場合、ImageSwitcherのoutAnimationが表示されない問題が発生します。
これはoutAnimationの開始タイミングが新しくsetImageされた瞬間だからです。

上記コードではsetImage前にrecycle,nullしているため、outAnimationを表示しようにもbitomapが存在しないため表示されないわけです。

もちろん、outAnimationが必要なければ上記コードで完了です。しかし、outAnimationを使わないのならImageSwitcherの意味はほとんどありませんから是非とも対応させたいところです。

ではどうするかというと、

bitmapを2つ用意する方法を取りました。

if(!bitmapFlag){
  if(bitmap!=null) {
    bitmap.recycle();
    bitmap = null;
  }
  bitmap = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]);
  bitmapDrawable = new BitmapDrawable(getResources() , bitmap);
}else{
  if(bitmap2!=null){
    bitmap2.recycle();
    bitmap2 = null;
  }
  bitmap2 = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]);
  bitmapDrawable = new BitmapDrawable(getResources() , bitmap2);
}
mImageSwitcher.setImageDrawable(bitmapDrawable);
bitmapFlag = !bitmapFlag;

bitmapとbitmap2を交互に使用します。

これでメモリが蓄積されることはありません。トップ画像の下側のようにほぼ直線のメモリ使用状況になります。

盛り上がっている箇所はinAnimationとoutAnimationの処理をしている時間帯です。

まとめ

AndroidでImageを扱う場合はAndroidProfilerでメモリ使用状況を必ず確認する。

bitmapは新たに値を入れる前、終了時にrecycle,nullを実行する。

outAnimationを使うならbitmapを2つ用意して交互にrecycleして使う。

スライドショーのソースコード

public class SlideShowActivity extends AppCompatActivity {

    ImageSwitcher mImageSwitcher;
    AnimationSet inAnimationSet;
    AnimationSet outAnimationSet;
    AlphaAnimation inAlphaAnimation;
    AlphaAnimation outAlphaAnimation;

    Bitmap bitmap;
    Bitmap bitmap2;
    BitmapDrawable bitmapDrawable;
    boolean bitmapFlag;


    int[] mImageResources = {R.drawable.slide00,R.drawable.slide01
            ,R.drawable.slide02,R.drawable.slide03
            ,R.drawable.slide04,R.drawable.slide05
            ,R.drawable.slide06,R.drawable.slide07
            ,R.drawable.slide08,R.drawable.slide09};

    int mPosition = 0;

    public class MainTimerTask extends TimerTask {
        @Override
        public void run() {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        movePosition();
                    }
                });
        }
    }
    Timer mTimer = new Timer();
    TimerTask mTimerTask = new MainTimerTask();
    Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mImageSwitcher = (ImageSwitcher) findViewById(R.id.imageSwitcher);
        mImageSwitcher.setFactory(new ViewSwitcher.ViewFactory() {
            @Override
            public View makeView() {
                ImageView imageView = new ImageView(getApplicationContext());
                return imageView;
            }
        });
        setMotion();

        mTimer.schedule(mTimerTask, 0, 4000);

    }
    private void setMotion(){

        inAlphaAnimation = new AlphaAnimation(0,1);
        inAlphaAnimation.setDuration(1500);
        inAlphaAnimation.setStartOffset(0);
        inAlphaAnimation.setInterpolator(new DecelerateInterpolator());
        inAnimationSet = new AnimationSet(false);
        inAnimationSet.addAnimation(inAlphaAnimation);

        outAlphaAnimation = new AlphaAnimation(1,0);
        outAlphaAnimation.setDuration(1000);
        outAlphaAnimation.setStartOffset(0);
        outAlphaAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        outAnimationSet = new AnimationSet(false);
        outAnimationSet.addAnimation(outAlphaAnimation);
        mImageSwitcher.setInAnimation(inAnimationSet);
        mImageSwitcher.setOutAnimation(outAnimationSet);

    }
    private void movePosition() {
        mPosition = mPosition + 1;
        if (mPosition >= mImageResources.length) {
            mPosition = 0;
        } 
        if(!bitmapFlag){
            if(bitmap!=null){
                bitmap.recycle();
                bitmap = null;
            }
            bitmap = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]);
            bitmapDrawable = new BitmapDrawable(getResources() , bitmap);

        }else{
            if(bitmap2!=null) {
                bitmap2.recycle();
                bitmap2 = null;
            }
            bitmap2 = BitmapFactory.decodeResource(getResources() , mImageResources[mPosition]);
            bitmapDrawable = new BitmapDrawable(getResources() , bitmap2);

        }
        mImageSwitcher.setImageDrawable(bitmapDrawable);
        bitmapFlag = !bitmapFlag;
    }
}

ぐるぐると順番通りにフェイドインフェイドアウトしながら表示するスライドショーです。