¿Cuándo debería reciclar un bitmap usando LRUCache?

Estoy usando un LRUCache para almacenar mapas de bits que están almacenados en el sistema de archivos. Construí el caché basado en los ejemplos aquí: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

El problema es que estoy viendo lockings de OutOfMemory con frecuencia mientras uso la aplicación. Creo que cuando LRUCache desaloja una imagen para dejar espacio a otra, la memoria no se libera.

Agregué una llamada a Bitmap.recycle () cuando se desaloja una imagen:

  // use 1/8 of the available memory for this memory cache final int cacheSize = 1024 * 1024 * memClass / 8; mImageCache = new LruCache(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; } }; 

Esto corrige los lockings, pero también da como resultado imágenes que a veces no aparecen en la aplicación (solo un espacio negro donde debería estar la imagen). En cualquier momento que ocurra, veo este mensaje en mi Logcat: Cannot generate texture from bitmap .

Una búsqueda rápida en Google revela que esto está sucediendo porque la imagen que se muestra ha sido reciclada.

Entonces, ¿Que esta pasando aquí? ¿Por qué las imágenes recicladas todavía están en LRUCache si solo las reciclo después de que se han eliminado? ¿Cuál es la alternativa para implementar un caché? Los documentos de Android indican claramente que LRUCache es el camino a seguir, pero no mencionan la necesidad de reciclar bitmaps o cómo hacerlo.

RESUELTO: En caso de que sea útil para cualquier otra persona, la solución a este problema sugerida por la respuesta aceptada es NO hacer lo que hice en el ejemplo de código anterior (no reciclar los mapas de bits en la llamada entryRemoved() ).

En cambio, cuando haya terminado con un ImageView (como onPause() en una actividad, o cuando una vista se haya reciclado en un adaptador) compruebe si el bitmap aún está en el caché (agregué un método isImageInCache() a mi caché clase) y, si no es así, recicla el bitmap. De lo contrario, déjalo en paz. Esto solucionó mis excepciones de OutOfMemory e impedía el reciclaje de mapas de bits que aún se estaban utilizando.

Creo que cuando LRUCache desaloja una imagen para dejar espacio a otra, la memoria no se libera.

No lo será, hasta que el Bitmap sea ​​reciclado o recogido.

Una búsqueda rápida en Google revela que esto está sucediendo porque la imagen que se muestra ha sido reciclada.

Por eso no deberías reciclar allí.

¿Por qué las imágenes recicladas todavía están en LRUCache si solo las reciclo después de que se han eliminado?

Presumiblemente, no están en el LRUCache . Están en un ImageView u otra cosa que aún está usando Bitmap .

¿Cuál es la alternativa para implementar un caché?

En aras de la argumentación, supongamos que está utilizando los objetos de Bitmap en los widgets de ImageView , como en las filas de un ListView .

Cuando haya terminado con un Bitmap (por ejemplo, se recicla la fila en un ListView ), verifique si todavía está en el caché. Si es así, lo dejas en paz. Si no lo es, lo recycle() .

La memoria caché simplemente le permite saber qué objetos Bitmap vale la pena conservar. La memoria caché no tiene forma de saber si el Bitmap todavía se usa en alguna parte.

Por cierto, si estás en el Nivel 11 de la API, considera usar inBitmap . OutOMemoryErrors se activan cuando no se puede cumplir una asignación. La última vez que lo comprobé, Android no tiene un recolector de basura compactable, por lo que puedes obtener un OutOfMemoryError debido a la fragmentación (quieres asignar algo más grande que el bloque individual más grande disponible).

Frente a lo mismo y gracias a @CommonsWare por la discusión. Publicando la solución completa aquí, así ayuda a más personas que vienen aquí por el mismo problema. Ediciones y comentarios son bienvenidos. Aclamaciones

  When should I recycle a bitmap using LRUCache? 
  • Precisamente cuando su bitmap no está en el caché ni se hace referencia a él desde ningún ImageView.

  • Para mantener el recuento de referencias del bitmap, debemos extender la clase BitmapDrawable y agregarle atributos de referencia.

  • Esta muestra de Android tiene la respuesta exactamente. DisplayingBitmaps.zip

Llegaremos al detalle y código a continuación.

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

No exactamente.

  • En la entrada Delegado eliminado, verifique si Bitmap todavía está referenciado desde cualquier ImageView. Si no. Recíclelo allí mismo.

  • Y viceversa, que se menciona en la respuesta aceptada que cuando la vista está por volverse a usar o ser objeto de dumping, compruebe que su bitmap (bitmap anterior si la vista se está reutilizando) está en la memoria caché. Si está allí, déjalo en paz, si no, recíbelo.

  • La clave aquí es que debemos verificar en ambos lugares si podemos reciclar bitmap o no.

Explicaré mi caso específico donde estoy usando LruCache para mantener bitmaps para mí. Y mostrarlos en ListView. Y llamando a reciclar en bitmaps cuando ya no están en uso.

RecyclingBitmapDrawable.java y RecyclingImageView.java de la muestra mencionada anteriormente son las piezas centrales que necesitamos aquí. Ellos están manejando las cosas hermosamente. Sus métodos setIsCached y setDisplayed están haciendo lo que necesitamos.

El código se puede encontrar en el enlace de muestra mencionado anteriormente. Pero también publica el código completo del archivo en la parte inferior de la respuesta en caso de que en el futuro el enlace se caiga o cambie. Realicé una pequeña modificación de la redefinición de setImageResource para verificar el estado del bitmap anterior.

— Aquí va el código para ti —

Entonces, su gerente de LruCache debería verse más o menos así.

LruCacheManager.java

 package com.example.cache; import android.os.Build; import android.support.v4.util.LruCache; public class LruCacheManager { private LruCache 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(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(); } } 

Y su getView () del adaptador ListView / GridView debería verse normal como siempre. Como cuando configura una nueva imagen en ImageView usando el método setImageDrawable. Está comprobando internamente la cuenta de referencia en el bitmap anterior y llamará a reciclar internamente si no está en 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; } 

Aquí está su 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); } } } } 

Aquí está su 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(); } }