56

LRUCacheファイルシステムに保存されているビットマップをキャッシュするためにを使用しています。ここの例に基づいてキャッシュを構築しました:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

問題は、アプリの使用中にOutOfMemoryが頻繁にクラッシュすることです。LRUCacheがイメージを削除して別のイメージ用のスペースを確保する場合、メモリは解放されていないと思います。

画像が削除されたときにBitmap.recycle()の呼び出しを追加しました。

  // use 1/8 of the available memory for this memory cache
    final int cacheSize = 1024 * 1024 * memClass / 8;
                mImageCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getByteCount();
                }

                @Override
                protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
                    oldBitmap.recycle();
                    oldBitmap = null;
                }
            };

これによりクラッシュが修正されますが、アプリに画像が表示されない場合もあります(画像があるはずの黒いスペースのみ)。発生するたびに、Logcatに次のメッセージが表示されますCannot generate texture from bitmap

クイックグーグル検索は、表示されている画像がリサイクルされているため、これが起こっていることを明らかにします。

では、ここで何が起こっているのでしょうか。削除された後にのみリサイクルするのに、リサイクルされた画像がLRUCacheに残っているのはなぜですか?キャッシュを実装するための代替手段は何ですか?Androidドキュメントには、LRUCacheが進むべき道であると明確に記載されていますが、ビットマップをリサイクルする必要性やその方法については言及されていません。

解決済み: 他の人に役立つ場合、受け入れられた回答によって提案されたこの問題の解決策は、上記のコード例で行ったことを実行しないことです(呼び出しでビットマップをリサイクルしないでくださいentryRemoved()

代わりに、ImageViewを終了したとき(onPause()アクティビティ内、またはビューがアダプターでリサイクルされたときなど)、ビットマップがまだキャッシュにあるかどうか(isImageInCache()キャッシュクラスにメソッドを追加した)、およびあるかどうかを確認します。そうでない場合は、ビットマップをリサイクルします。それ以外の場合は、そのままにしておきます。これにより、私の例外が修正され、OutOfMemoryまだ使用されているビットマップのリサイクルが防止されました。

4

2 に答える 2

41

LRUCacheがイメージを削除して別のイメージ用のスペースを確保する場合、メモリは解放されていないと思います。

Bitmapがリサイクルされるかガベージコレクションされるまで、そうはなりません。

クイックグーグル検索は、表示されている画像がリサイクルされているため、これが起こっていることを明らかにします。

そのため、そこでリサイクルするべきではありません。

削除された後にのみリサイクルするのに、リサイクルされた画像がLRUCacheに残っているのはなぜですか?

おそらく、それらはにありませんLRUCache。それらは、ImageViewまだを使用しているまたは何か他のものにありBitmapます。

キャッシュを実装するための代替手段は何ですか?

議論のために、の行などのウィジェットでBitmapオブジェクトを使用していると仮定しましょう。ImageViewListView

Bitmap(たとえば、aの行がリサイクルされる)が完了しListViewたら、それがまだキャッシュにあるかどうかを確認します。もしそうなら、あなたはそれを放っておいてください。そうでない場合は、あなたですrecycle()

キャッシュは、Bitmap保持する価値のあるオブジェクトを通知するだけです。キャッシュには、Bitmapがまだどこかで使用されているかどうかを知る方法がありません。

ところで、APIレベル11以上を使用している場合は、の使用を検討してinBitmapください。OutOMemoryErrors割り当てを実行できない場合にトリガーされます。最後に確認したところ、Androidには圧縮ガベージコレクターがないOutOfMemoryErrorため、断片化が原因で発生する可能性があります(使用可能な最大の単一ブロックよりも大きなものを割り当てたい)。

于 2012-05-25T00:20:46.053 に答える
18

同じことに直面し、議論してくれた@CommonsWareに感謝します。ここに完全な解決策を投稿して、同じ問題でより多くの人がここに来るのを助けます。編集とコメントを歓迎します。乾杯

 When should I recycle a bitmap using LRUCache?
  • 正確には、ビットマップがキャッシュになく、ImageViewから参照されていない場合。

  • ビットマップの参照カウントを維持するには、BitmapDrawableクラスを拡張し、それらに参照属性を追加する必要があります。

  • このAndroidサンプルには、正確な答えがあります。 DisplayingBitmaps.zip

以下に詳細とコードを示します。

(don't recycle the bitmaps in the entryRemoved() call).

ではない正確に。

  • entryRemovedデリゲートで、ビットマップがどのImageViewからも参照されているかどうかを確認します。そうでない場合。そこでリサイクルしてください。

  • 受け入れられた回答で言及されている逆もまた同様です。ビューが再利用されようとしているとき、またはダンプされているときは、そのビットマップ(ビューが再利用されている場合は前のビットマップ)がキャッシュにあることを確認してください。ある場合はそのままにして、それ以外の場合はリサイクルしてください。

  • ここで重要なのは、ビットマップをリサイクルできるかどうかを両方の場所で確認する必要があるということです。

LruCacheを使用してビットマップを保持している特定のケースについて説明します。そしてそれらをListViewに表示します。そして、使用されなくなったビットマップでリサイクルを呼び出します。

上記のサンプルのRecyclingBitmapDrawable.javaRecyclingImageView.javaは、ここで必要なコア部分です。彼らは物事を美しく扱っています。それらのsetIsCachedメソッドとsetIsDisplayedメソッドは、必要なことを実行しています。

コードは、上記のサンプルリンクにあります。ただし、将来リンクがダウンしたり変更されたりした場合に備えて、回答の下部にファイルの完全なコードを投稿することもできます。以前のビットマップの状態をチェックするために、setImageResourceのオーバーライドも少し変更しました。

---ここにあなたのためのコードがあります---

したがって、LruCacheマネージャーは次のようになります。

LruCacheManager.java

package com.example.cache;

import android.os.Build;
import android.support.v4.util.LruCache;

public class LruCacheManager {

    private LruCache<String, RecyclingBitmapDrawable> mMemoryCache;

    private static LruCacheManager instance;

    public static LruCacheManager getInstance() {
        if(instance == null) {
            instance = new LruCacheManager();
            instance.init();
        } 

        return instance;
    }

    private void init() {

        // We are declaring a cache of 6Mb for our use.
        // You need to calculate this on the basis of your need 
        mMemoryCache = new LruCache<String, RecyclingBitmapDrawable>(6 * 1024 * 1024) {
            @Override
            protected int sizeOf(String key, RecyclingBitmapDrawable bitmapDrawable) {
                // The cache size will be measured in kilobytes rather than
                // number of items.
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
                    return bitmapDrawable.getBitmap().getByteCount() ;
                } else {
                    return bitmapDrawable.getBitmap().getRowBytes() * bitmapDrawable.getBitmap().getHeight();
                }
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) {
                super.entryRemoved(evicted, key, oldValue, newValue);
                oldValue.setIsCached(false);
            }
        };

    }

    public void addBitmapToMemoryCache(String key, RecyclingBitmapDrawable bitmapDrawable) {
        if (getBitmapFromMemCache(key) == null) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been added into the memory cache
            bitmapDrawable.setIsCached(true);
            mMemoryCache.put(key, bitmapDrawable);
        }
    }

    public RecyclingBitmapDrawable getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    public void clear() {
        mMemoryCache.evictAll();
    }
}


また、 ListView / GridViewアダプタのgetView()は、通常のように正常に見えるはずです。setImageDrawableメソッドを使用してImageViewに新しい画像を設定する場合と同じです。以前のビットマップの参照カウントを内部的にチェックし、lrucacheにない場合は内部でrecycleを呼び出します。

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        RecyclingImageView imageView;
        if (convertView == null) { // if it's not recycled, initialize some attributes
            imageView = new RecyclingImageView(getActivity());
            imageView.setLayoutParams(new GridView.LayoutParams(
                    GridView.LayoutParams.WRAP_CONTENT,
                    GridView.LayoutParams.WRAP_CONTENT));
            imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
            imageView.setPadding(5, 5, 5, 5);

        } else {
            imageView = (RecyclingImageView) convertView;
        }

        MyDataObject dataItem = (MyDataObject) getItem(position);
        RecyclingBitmapDrawable  image = lruCacheManager.getBitmapFromMemCache(dataItem.getId());

        if(image != null) {
            // This internally is checking reference count on previous bitmap it used.
            imageView.setImageDrawable(image);
        } else {
            // You have to implement this method as per your code structure.
            // But it basically doing is preparing bitmap in the background
            // and adding that to LruCache.
            // Also it is setting the empty view till bitmap gets loaded.
            // once loaded it just need to call notifyDataSetChanged of adapter. 
            loadImage(dataItem.getId(), R.drawable.empty_view);
        }

        return imageView;

    }

これがRecyclingImageView.javaです

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.cache;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;


/**
 * Sub-class of ImageView which automatically notifies the drawable when it is
 * being displayed.
 */
public class RecyclingImageView extends ImageView {

    public RecyclingImageView(Context context) {
        super(context);
    }

    public RecyclingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * @see android.widget.ImageView#onDetachedFromWindow()
     */
    @Override
    protected void onDetachedFromWindow() {
        // This has been detached from Window, so clear the drawable
        setImageDrawable(null);

        super.onDetachedFromWindow();
    }

    /**
     * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageDrawable(Drawable drawable) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        // Call super to set new Drawable
        super.setImageDrawable(drawable);

        // Notify new Drawable that it is being displayed
        notifyDrawable(drawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }

    /**
     * @see android.widget.ImageView#setImageResource(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageResource(int resId) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        // Call super to set new Drawable
        super.setImageResource(resId);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }


    /**
     * Notifies the drawable that it's displayed state has changed.
     *
     * @param drawable
     * @param isDisplayed
     */
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable instanceof RecyclingBitmapDrawable) {
            // The drawable is a CountingBitmapDrawable, so notify it
            ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
        } else if (drawable instanceof LayerDrawable) {
            // The drawable is a LayerDrawable, so recurse on each layer
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
            }
        }
    }

}

これがRecyclingBitmapDrawable.javaです

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.cache;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;

import android.util.Log;

/**
 * A BitmapDrawable that keeps track of whether it is being displayed or cached.
 * When the drawable is no longer being displayed or cached,
 * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
 */
public class RecyclingBitmapDrawable extends BitmapDrawable {

    static final String TAG = "CountingBitmapDrawable";

    private int mCacheRefCount = 0;
    private int mDisplayRefCount = 0;

    private boolean mHasBeenDisplayed;

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    /**
     * Notify the drawable that the displayed state has changed. Internally a
     * count is kept so that the drawable knows when it is no longer being
     * displayed.
     *
     * @param isDisplayed - Whether the drawable is being displayed or not
     */
    public void setIsDisplayed(boolean isDisplayed) {
        //BEGIN_INCLUDE(set_is_displayed)
        synchronized (this) {
            if (isDisplayed) {
                mDisplayRefCount++;
                mHasBeenDisplayed = true;
            } else {
                mDisplayRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
        //END_INCLUDE(set_is_displayed)
    }

    /**
     * Notify the drawable that the cache state has changed. Internally a count
     * is kept so that the drawable knows when it is no longer being cached.
     *
     * @param isCached - Whether the drawable is being cached or not
     */
    public void setIsCached(boolean isCached) {
        //BEGIN_INCLUDE(set_is_cached)
        synchronized (this) {
            if (isCached) {
                mCacheRefCount++;
            } else {
                mCacheRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
        //END_INCLUDE(set_is_cached)
    }

    private synchronized void checkState() {
        //BEGIN_INCLUDE(check_state)
        // If the drawable cache and display ref counts = 0, and this drawable
        // has been displayed, then recycle
        if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
                && hasValidBitmap()) {

            Log.d(TAG, "No longer being used or cached so recycling. "
                        + toString());

        getBitmap().recycle();
    }
        //END_INCLUDE(check_state)
    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}
于 2014-04-04T07:10:20.953 に答える