android - library - ¿Cuándo debo reciclar un mapa de bits usando LRUCache?
image library android (2)
Creo que cuando el LRUCache desaloja una imagen para dejar espacio para otra, la memoria no se libera.
No lo será, hasta que el Bitmap
sea reciclado o recolectado en la basura.
Una búsqueda rápida en Google revela que esto está sucediendo porque la imagen que se muestra se ha reciclado.
Por eso no deberías estar reciclando allí.
¿Por qué las imágenes recicladas aún están en el LRUCache si solo las estoy reciclando después de que se hayan eliminado?
Presumiblemente, no están en el LRUCache
. Están en un ImageView
o en otra cosa que todavía está utilizando el Bitmap
.
¿Cuál es la alternativa para implementar un caché?
En aras del argumento, 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, la fila en un ListView
se recicla), verifica si todavía está en el caché. Si lo es, déjalo en paz. Si no lo es, lo recycle()
.
El caché simplemente le permite saber a qué objetos de Bitmap
vale la pena mantenerlos. El caché no tiene forma de saber si el Bitmap
todavía se está utilizando en algún lugar.
Por cierto, si está en el nivel API 11+, considere usar inBitmap
. OutOMemoryErrors
se desencadena cuando no se puede cumplir una asignación. La última vez que lo verifiqué, Android no tiene un recolector de basura compacto, por lo que puede obtener un OutOfMemoryError
debido a la fragmentación (desea asignar algo más grande que el bloque único más grande disponible).
Estoy usando un LRUCache
para almacenar en caché los mapas de bits que se almacenan 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 veo que OutOfMemory se bloquea con frecuencia mientras uso la aplicación. Creo que cuando el LRUCache desaloja una imagen para dejar espacio para otra, la memoria no se libera.
Agregué una llamada a Bitmap.recycle () cuando una imagen es desalojada:
// 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;
}
};
Esto soluciona los bloqueos, sin embargo, también puede ocasionar que las imágenes no aparezcan en la aplicación (solo un espacio negro donde debería estar la imagen). Cada vez que ocurre, 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 se ha reciclado.
Entonces, ¿Que esta pasando aquí? ¿Por qué las imágenes recicladas aún están en el LRUCache si solo las estoy reciclando después de que se hayan 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 los mapas de bits ni 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 recicle los mapas de bits en la llamada entryRemoved()
).
En su lugar, cuando termine con un ImageView (como onPause()
en una actividad, o cuando una vista se recicle en un adaptador) verifique si el mapa de bits todavía está en la caché (agregué un método isImageInCache()
a mi caché clase) y, si no lo es, recicle el mapa de bits. De lo contrario, déjalo en paz. Esto solucionó mis excepciones de OutOfMemory
e impidió el reciclaje de los mapas de bits que aún se estaban utilizando.
Enfrentó lo mismo y gracias a @CommonsWare por la discusión. Publicar la solución completa aquí para que ayude a más personas que vienen aquí por el mismo problema. Se aceptan ediciones y comentarios. Aclamaciones
When should I recycle a bitmap using LRUCache?
Precisamente cuando su mapa de bits no se encuentra en la memoria caché y no se hace referencia a ningún ImageView.
Para mantener el recuento de referencia del mapa de bits, tenemos que ampliar la clase BitmapDrawable y agregarles atributos de referencia.
Esta muestra de Android tiene la respuesta exacta. DisplayingBitmaps.zip
Llegaremos a los detalles y código abajo.
(don''t recycle the bitmaps in the entryRemoved() call).
No exactamente.
En entryRemoved delegate, compruebe si Bitmap aún está referenciado desde cualquier ImageView. Si no. Reciclarlo allí mismo.
Y viceversa, que se menciona en la respuesta aceptada de que cuando la vista está a punto de ser reutilizada o volcada, verifique su mapa de bits (el mapa de bits anterior si la vista se reutiliza) está en el caché. Si está ahí, déjalo solo o recicla.
La clave aquí es que debemos verificar en ambos lugares si podemos reciclar el mapa de bits o no.
Explicaré mi caso específico en el que estoy usando LruCache para guardar mapas de bits para mí. Y mostrándolos en ListView. Y llamar a reciclar en mapas de bits cuando ya no están en uso.
RecyclingBitmapDrawable.java y RecyclingImageView.java del ejemplo mencionado anteriormente son las piezas principales que necesitamos aquí. Ellos están manejando las cosas muy bien. Sus métodos setIsCached y setIsDisplayed están haciendo lo que necesitamos.
El código se puede encontrar en el enlace de muestra mencionado anteriormente. Pero también se 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. También se hizo una pequeña modificación de anular setImageResource para verificar el estado del mapa de bits anterior.
--- Aquí va el código para ti ---
Así que tu administrador de LruCache debería tener un aspecto como este.
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();
}
}
Y su adaptador getView () de ListView / GridView debería verse normal como siempre. Como cuando está configurando una nueva imagen en ImageView usando el método setImageDrawable. Se comprueba internamente el recuento de referencia en el mapa de bits anterior y se invoca el reciclaje 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();
}
}