wake-up-neo.com

Wann sollte ich eine Bitmap mit LRUCache recyceln?

Ich verwende ein LRUCache, um Bitmaps zwischenzuspeichern, die im Dateisystem gespeichert sind. Ich habe den Cache anhand der folgenden Beispiele erstellt: http://developer.Android.com/training/displaying-bitmaps/cache-bitmap.html

Das Problem ist, dass OutOfMemory während der Verwendung der App häufig abstürzt. Ich glaube, wenn der LRUCache ein Bild entfernt, um Platz für ein anderes zu schaffen, wird die Erinnerung nicht freigegeben.

Ich habe Bitmap.recycle () aufgerufen, wenn ein Bild entfernt wurde:

  // 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;
                }
            };

Dies behebt die Abstürze, führt jedoch auch dazu, dass Bilder manchmal nicht in der App angezeigt werden (nur ein schwarzer Bereich, in dem sich das Bild befinden sollte). Bei jedem Auftreten sehe ich diese Meldung in meinem Logcat: Cannot generate texture from bitmap.

Eine schnelle Google-Suche zeigt, dass dies geschieht, weil das angezeigte Bild recycelt wurde.

Also, was ist hier los? Warum befinden sich recycelte Bilder immer noch im LRUCache, wenn ich sie erst nach dem Entfernen recycle? Was ist die Alternative zum Implementieren eines Caches? In den Android docs wird klar angegeben, dass LRUCache der richtige Weg ist, aber es wird nicht erwähnt, dass Bitmaps recycelt werden müssen oder wie dies getan wird.

BEHOBEN: Falls es für andere nützlich ist, lautet die in der akzeptierten Antwort vorgeschlagene Lösung für dieses Problem [~ # ~] nicht [~ # ~] tun, was ich im obigen Codebeispiel getan habe (recyceln Sie die Bitmaps nicht im entryRemoved()-Aufruf).

Überprüfen Sie stattdessen, ob sich die Bitmap noch im Cache befindet, wenn Sie mit einer ImageView (z. B. onPause() in einer Aktivität fertig sind oder wenn eine Ansicht in einem Adapter wiederverwendet wird (ich habe eine isImageInCache() -Methode in meine Cache-Klasse) und, falls dies nicht der Fall ist, recyceln Sie die Bitmap. Ansonsten lass es in Ruhe. Dies behebt meine OutOfMemory Ausnahmen und verhindert das Recycling von Bitmaps, die noch verwendet werden.

54
howettl

Ich glaube, wenn der LRUCache ein Bild entfernt, um Platz für ein anderes zu schaffen, wird die Erinnerung nicht freigegeben.

Es wird nicht sein, bis das Bitmap recycelt oder Müll gesammelt wird.

Eine schnelle Google-Suche zeigt, dass dies geschieht, weil das angezeigte Bild recycelt wurde.

Deshalb sollten Sie dort nicht recyceln.

Warum befinden sich recycelte Bilder immer noch im LRUCache, wenn ich sie erst nach dem Entfernen recycle?

Vermutlich befinden sie sich nicht im LRUCache. Sie befinden sich in einem ImageView oder in etwas anderem, das noch das Bitmap verwendet.

Was ist die Alternative zum Implementieren eines Caches?

Nehmen wir als Argument an, Sie verwenden die Bitmap -Objekte in ImageView-Widgets, z. B. in Zeilen eines ListView.

Wenn Sie mit einem Bitmap fertig sind (z. B. wenn eine Zeile in einem ListView wiederverwendet wird), überprüfen Sie, ob es sich noch im Cache befindet. Wenn ja, lassen Sie es in Ruhe. Ist dies nicht der Fall, recycle() Sie es.

Der Cache informiert Sie lediglich darüber, an welchen Bitmap Objekten es sich zu halten lohnt. Der Cache kann nicht erkennen, ob das Bitmap noch irgendwo verwendet wird.

Übrigens, wenn Sie auf API Level 11+ sind, ziehen Sie die Verwendung von inBitmap in Betracht. OutOMemoryErrors werden ausgelöst wenn eine Zuordnung nicht erfüllt werden kann. Zuletzt habe ich überprüft, ob Android hat keinen Garbage Collector zum Komprimieren, sodass Sie aufgrund von Fragmentierung ein OutOfMemoryError erhalten können (Sie möchten etwas Größeres als den größten verfügbaren Einzelblock zuweisen).

41
CommonsWare

Konfrontiert das gleiche und danke an @CommonsWare für die Diskussion. Wenn Sie die vollständige Lösung hier veröffentlichen, können mehr Personen für dasselbe Problem hierher kommen. Änderungen und Kommentare sind willkommen. Prost

 When should I recycle a bitmap using LRUCache?
  • Genau dann, wenn sich Ihre Bitmap weder im Cache befindet noch von einer ImageView referenziert wird.

  • Um die Referenzanzahl der Bitmap beizubehalten, müssen wir die BitmapDrawable-Klasse erweitern und ihnen Referenzattribute hinzufügen.

  • Dieses Android Beispiel hat genau die Antwort darauf. DisplayingBitmaps.Zip

Wir werden auf das Detail und den Code weiter unten eingehen.

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

Nicht genau.

  • Überprüfen Sie in entryRemoved delegate, ob in ImageView noch auf Bitmap verwiesen wird. Wenn nicht. Bereiten Sie es dort selbst auf.

  • Und umgekehrt, was in der akzeptierten Antwort erwähnt wird, dass sich die Bitmap (vorherige Bitmap, wenn die Ansicht wiederverwendet wird) im Cache befindet, wenn die Ansicht wiederverwendet oder gelöscht wird. Wenn es da ist, lass es in Ruhe, sonst recycel es.

  • Der Schlüssel hier ist, dass wir an beiden Stellen prüfen müssen, ob wir die Bitmap recyceln können oder nicht.

Ich werde meinen speziellen Fall erklären, in dem ich LruCache verwende, um Bitmaps für mich zu speichern. Und sie in ListView anzeigen. Rufen Sie recycle on bitmaps auf, wenn diese nicht mehr verwendet werden.

RecyclingBitmapDrawable.Java und RecyclingImageView.Java von dem oben erwähnten Beispiel sind die Kernstücke, die wir hier brauchen. Sie handhaben die Dinge wunderbar. Ihr setIsCached und setIsDisplayed Methoden tun, was wir brauchen.

Code finden Sie im oben genannten Beispiellink. Sie können aber auch den vollständigen Code der Datei unten in der Antwort eintragen, falls der Link in Zukunft ausfällt oder geändert wird. Es wurde eine kleine Änderung am Überschreiben von setImageResource vorgenommen, um auch den Status der vorherigen Bitmap zu überprüfen.

--- Hier geht der Code für dich ---

Ihr LruCache-Manager sollte also ungefähr so ​​aussehen.

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();
    }
}


Und dein getView () des ListView/GridView Adapters sollte normal aussehen wie gewohnt. Wie beim Festlegen eines neuen Bilds in ImageView mithilfe der setImageDrawable-Methode. Es überprüft intern den Referenzzähler auf der vorherigen Bitmap und ruft Recycle intern auf, wenn nicht in lrucache.

@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;

    }

Hier ist dein 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);
            }
        }
    }

}

Hier ist dein 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();
    }

}
17
Javanator