java.lang.OutOfMemoryError: el tamaño del mapa de bits supera el presupuesto de VM - Android

159

Desarrollé una aplicación que usa muchas imágenes en Android.

La aplicación se ejecuta una vez, llena la información en la pantalla ( Layouts, Listviews, Textviews, ImageViews, etc) y el usuario lee la información.

No hay animación, ni efectos especiales ni nada que pueda llenar la memoria. A veces los elementos dibujables pueden cambiar. Algunos son recursos de Android y otros son archivos guardados en una carpeta en la SDCARD.

Luego, el usuario se cierra (el onDestroyVM ejecuta el método y la aplicación permanece en la memoria) y luego, en algún momento, el usuario ingresa nuevamente.

Cada vez que el usuario ingresa a la aplicación, puedo ver que la memoria crece cada vez más hasta que el usuario obtiene la java.lang.OutOfMemoryError.

Entonces, ¿cuál es la mejor / correcta forma de manejar muchas imágenes?

¿Debo ponerlos en métodos estáticos para que no se carguen todo el tiempo? ¿Tengo que limpiar el diseño o las imágenes utilizadas en el diseño de una manera especial?

Daniel Benedykt
fuente
44
Esto puede ayudar si tiene muchos cambios que se pueden cambiar. Está funcionando desde que hice el programa yo mismo :) androidactivity.wordpress.com/2011/09/24/…

Respuestas:

72

Parece que tienes una pérdida de memoria. El problema no es manejar muchas imágenes, es que sus imágenes no se desasignan cuando se destruye su actividad.

Es difícil decir por qué esto es sin mirar su código. Sin embargo, este artículo tiene algunos consejos que pueden ayudar:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

En particular, el uso de variables estáticas puede empeorar las cosas, no mejorarlas. Es posible que deba agregar un código que elimine las devoluciones de llamada cuando se redibuje su aplicación, pero nuevamente, aquí no hay suficiente información para asegurarlo.

Trevor Johns
fuente
8
Esto es cierto y también tener muchas imágenes (y no una pérdida de memoria) puede causar que OutOfMemory se deba a múltiples razones, como la ejecución de muchas otras aplicaciones, la fragmentación de la memoria, etc. sistemáticamente, como hacer siempre lo mismo, entonces es una fuga. Si lo obtienes una vez al día o algo así, es porque estás demasiado cerca del límite. Mi sugerencia en este caso es intentar eliminar algunas cosas e intentar nuevamente. Además, la memoria de imagen está fuera de la memoria del montón, así que tenga cuidado con ambos.
Daniel Benedykt
15
Si sospecha una pérdida de memoria, una forma rápida de monitorear el uso del montón es a través del comando adb shell dumpsys meminfo. Si ve que el uso del almacenamiento dinámico aumenta en unos pocos ciclos de gc (GC_ * log row en logcat), puede estar bastante seguro de que tiene una fuga. Luego, cree un volcado de almacenamiento dinámico (o varios en diferentes momentos) a través de adb o DDMS y analícelo a través de las herramientas del árbol dominador en Eclipse MAT. Pronto debería encontrar qué objeto tiene un montón de retenciones que crece con el tiempo. Esa es tu pérdida de memoria. Escribí un artículo con más detalles aquí: macgyverdev.blogspot.com/2011/11/…
Johan Norén
98

Uno de los errores más comunes que encontré al desarrollar aplicaciones de Android es el error "java.lang.OutOfMemoryError: el tamaño del mapa de bits supera el presupuesto de VM". Encontré este error con frecuencia en actividades que utilizan muchos mapas de bits después de cambiar la orientación: la Actividad se destruye, se crea nuevamente y los diseños se "inflan" desde el XML que consume la memoria de VM disponible para mapas de bits.

El recolector de basura no asigna correctamente los mapas de bits en el diseño de la actividad anterior porque han cruzado referencias a su actividad. Después de muchos experimentos, encontré una solución bastante buena para este problema.

Primero, establezca el atributo "id" en la vista principal de su diseño XML:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

Luego, en el onDestroy() método de su Actividad, llame al unbindDrawables()método pasando una referencia a la Vista principal y luego haga un System.gc().

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

Este unbindDrawables()método explora el árbol de vista de forma recursiva y:

  1. Elimina devoluciones de llamada en todos los elementos de fondo
  2. Elimina niños en cada grupo de vista
HP android
fuente
10
Excepto ... java.lang.UnsupportedOperationException: removeAllViews () no es compatible con AdapterView
Gracias, esto ayuda a reducir el tamaño de mi montón de manera significativa ya que tengo muchos elementos extraíbles ... @GJTorikian para eso solo intente atrapar en removeAllViews () la mayoría de las subclases de ViewGroup funcionan bien.
Eric Chen
55
O simplemente cambie la condición: if (ver instancia de ViewGroup &&! (Ver instancia de AdapterView))
Anke
2
@ Adam Varhegyi, puede llamar explícitamente la misma función para la vista de galería en onDestroy. Como unbindDrawables (galleryView); espero que esto funcione para ti ...
hp.android
11

Para evitar este problema, puede utilizar el método nativo Bitmap.recycle()antes null-ing Bitmapobjeto (o establecer otro valor). Ejemplo:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

Y luego puedes cambiar myBitmapsin llamar System.gc()como:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
ddmytrenko
fuente
1
esto no funcionará si intenta agregar esos elementos en una vista de lista. ¿Tienes alguna sugerencia al respecto?
Rafael Sanches
@ddmytrenko Estás reciclando la imagen antes de asignarla. ¿No evitaría eso que sus píxeles se procesen?
IgorGanapolsky
8

Me he encontrado con este problema exacto. El montón es bastante pequeño, por lo que estas imágenes pueden descontrolarse bastante rápido en lo que respecta a la memoria. Una forma es darle al recolector de basura una pista para recopilar memoria en un mapa de bits llamando a su método de reciclaje .

Además, no se garantiza que se llame al método onDestroy. Es posible que desee mover esta lógica / limpieza a la actividad onPause. Consulte el diagrama / tabla del ciclo de vida de la actividad en esta página para obtener más información.

Donn Felker
fuente
Gracias por la info. Estoy administrando todos los Drawables y no Bitmaps, por lo que no puedo llamar a Reciclar. ¿Alguna otra sugerencia para los dibujables? Gracias Daniel
Daniel Benedykt
Gracias por la onPausesugerencia Estoy usando 4 pestañas con Bitmapimágenes en todas ellas, por lo que destruir la actividad podría no ocurrir demasiado pronto (si el usuario navega por todas las pestañas).
Azurespot
7

Esta explicación podría ayudar: http://code.google.com/p/android/issues/detail?id=8488#c80

"Consejos rápidos:

1) NUNCA llame a System.gc () usted mismo. Esto se ha propagado como una solución aquí, y no funciona. No lo hagas. Si se dio cuenta en mi explicación, antes de obtener un OutOfMemoryError, la JVM ya ejecuta una recolección de basura, por lo que no hay razón para volver a hacerlo (está ralentizando su programa). Hacer uno al final de su actividad es solo encubrir el problema. Puede causar que el mapa de bits se coloque en la cola del finalizador más rápido, pero no hay ninguna razón por la que no podría haber llamado simplemente reciclar en cada mapa de bits.

2) Siempre llame a recycle () en mapas de bits que ya no necesita. Como mínimo, en onDestroy de su actividad, revise y recicle todos los mapas de bits que estaba utilizando. Además, si desea que las instancias de mapa de bits se recopilen del montón de dalvik más rápido, no está de más borrar cualquier referencia al mapa de bits.

3) Llamar a recycle () y luego System.gc () aún podría no eliminar el mapa de bits del montón de Dalvik. No se preocupe por esto. recycle () hizo su trabajo y liberó la memoria nativa, solo llevará algún tiempo seguir los pasos que describí anteriormente para eliminar realmente el mapa de bits del montón de Dalvik. ¡Esto NO es un gran problema porque la gran parte de la memoria nativa ya es gratuita!

4) Siempre asuma que hay un error en el marco pasado. Dalvik está haciendo exactamente lo que se supone que debe hacer. Puede que no sea lo que esperas o lo que quieres, pero es cómo funciona. "

Aron
fuente
5

Tuve exactamente el mismo problema. Después de algunas pruebas, descubrí que este error aparece para el escalado de imágenes grandes. Reduje la escala de la imagen y el problema desapareció.

PD: Al principio intenté reducir el tamaño de la imagen sin reducirla. Eso no detuvo el error.

GSree
fuente
44
¿podría publicar el código sobre cómo escaló la imagen? Estoy enfrentando el mismo problema y creo que esto podría resolverlo. ¡Gracias!
Gligor
@Gix: creo que quiere decir que tuvo que reducir el tamaño de las imágenes antes de incorporarlas a los recursos del proyecto, reduciendo así la huella de memoria de los elementos dibujables. Para esto, debe usar el editor de fotos que prefiera. Me gusta pixlr.com
Kyle Clegg
5

Los siguientes puntos realmente me ayudaron mucho. Puede haber otros puntos también, pero estos son muy cruciales:

  1. Utilice el contexto de la aplicación (en lugar de activity.this) siempre que sea posible.
  2. Pare y libere sus hilos en el método de actividad onPause ()
  3. Libere sus vistas / devoluciones de llamada en el método de actividad onDestroy ()

fuente
4

Sugiero una forma conveniente de resolver este problema. Simplemente asigne el valor del atributo "android: configChanges" como se sigue en Mainfest.xml para su actividad con errores. Me gusta esto:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

La primera solución que ofrecí realmente había reducido la frecuencia de error OOM a un nivel bajo. Pero, no resolvió el problema totalmente. Y luego daré la segunda solución:

Como detalla la OOM, he usado demasiada memoria de tiempo de ejecución. Entonces, reduzco el tamaño de la imagen en ~ / res / drawable de mi proyecto. Tal como una imagen sobrecalificada que tiene una resolución de 128X128, podría redimensionarse a 64x64, lo que también sería adecuado para mi aplicación. Y después de hacerlo con un montón de imágenes, el error OOM no vuelve a ocurrir.

Ranger Way
fuente
1
Esto funciona porque estás evitando el reinicio de tu aplicación. Por lo tanto, no es tanto una solución como una evitación. Pero a veces eso es todo lo que necesitas. Y el método predeterminado onConfigurationChanged () en Activity cambiará su orientación por usted, por lo que si no necesita ningún cambio en la interfaz de usuario para adaptarse realmente a las diferentes orientaciones, puede estar perfectamente satisfecho con el resultado. Sin embargo, ten cuidado con otras cosas que pueden reiniciarte. Es posible que desee utilizar esto: android: configChanges = "keyboardHidden | orientación | teclado | locale | mcc | mnc | pantalla táctil | screenLayout | fontScale"
Carl
3

Yo también estoy frustrado por el error de memoria. Y sí, también descubrí que este error aparece mucho al escalar imágenes. Al principio intenté crear tamaños de imagen para todas las densidades, pero descubrí que esto aumentaba sustancialmente el tamaño de mi aplicación. Así que ahora solo estoy usando una imagen para todas las densidades y escalando mis imágenes.

Mi aplicación arrojaría un error de memoria cada vez que el usuario pasara de una actividad a otra. Establecer mis elementos dibujables en nulo y llamar a System.gc () no funcionó, ni reciclar mis bitmapDrawables con getBitMap (). Recycle (). Android continuaría arrojando el error de memoria con el primer enfoque, y lanzaría un mensaje de error de lienzo cada vez que intentara usar un mapa de bits reciclado con el segundo enfoque.

Adopté un tercer enfoque. Configuré todas las vistas como nulas y el fondo como negro. Hago esta limpieza en mi método onStop (). Este es el método que se llama tan pronto como la actividad ya no es visible. Si lo hace en el método onPause (), los usuarios verán un fondo negro. No es ideal. En cuanto a hacerlo en el método onDestroy (), no hay garantía de que se llame.

Para evitar que se produzca una pantalla negra si el usuario presiona el botón Atrás en el dispositivo, recargo la actividad en el método onRestart () llamando a los métodos startActivity (getIntent ()) y luego finish ().

Nota: no es realmente necesario cambiar el fondo a negro.

alegremente
fuente
1

Los métodos BitmapFactory.decode *, discutidos en la lección Cargar mapas de bits grandes de manera eficiente , no deben ejecutarse en el hilo principal de la interfaz de usuario si los datos de origen se leen desde el disco o una ubicación de red (o realmente cualquier otra fuente que no sea memoria). El tiempo que tardan en cargar estos datos es impredecible y depende de una variedad de factores (velocidad de lectura desde el disco o la red, tamaño de la imagen, potencia de la CPU, etc.). Si una de estas tareas bloquea el hilo de la interfaz de usuario, el sistema marca su aplicación como no receptiva y el usuario tiene la opción de cerrarla (consulte Diseño de capacidad de respuesta para obtener más información).

Li3ro
fuente
0

Bueno, he intentado todo lo que encontré en Internet y ninguno de ellos funcionó. Llamar a System.gc () solo reduce la velocidad de la aplicación. Reciclar mapas de bits en onDestroy tampoco funcionó para mí.

Lo único que funciona ahora es tener una lista estática de todos los mapas de bits para que los mapas de bits sobrevivan después de un reinicio. Y solo use los mapas de bits guardados en lugar de crear nuevos cada vez que se reinicie la actividad.

En mi caso, el código se ve así:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}
HarryHao
fuente
¿No filtraría esto tu contexto? (datos de la actividad)
Rafael Sanches
0

Tuve el mismo problema solo con cambiar las imágenes de fondo con tamaños razonables. Obtuve mejores resultados al configurar ImageView en nulo antes de poner una nueva imagen.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
Gunnar Bernstein
fuente
0

FWIW, aquí hay un bitmap-cache ligero que codifiqué y he usado durante algunos meses. No son todas las campanas y silbatos, así que lea el código antes de usarlo.

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
Guy Smith
fuente