Solución a Exception de Android: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

Este post quiza sea demasiado especifico para crear con él una nueva entrada, pero creo sinceramente que se lo merece. Este problema, o excepcion que saltaba en uno de mis juegos, me ha quitado varias decenas de horas de mi vida, así que espero poder evitar este perdida a muchos de vosotros.

Explicacion del problema:

Esta exception salta, cuando se cargan demasidas imagenes dentro de una aplicacion android. El limite medio de una aplicacion esta entre 16Mb y 24Mb dependiendo del dispositivo. Cuando se soprepasa el limite la maquina dalvik fuerza el cierre de la aplicacion y en el logCat obtenemos algo parecido a esto:

11-12 18:15:01.932: D/dalvikvm(411): GC_FOR_MALLOC freed 4K, 50% free 3523K/6919K, external 16605K/17130K, paused 70ms
11-12 18:15:01.962: D/skia(411): --- decoder->decode returned false
11-12 18:15:01.972: D/AndroidRuntime(411): Shutting down VM
11-12 18:15:01.972: W/dalvikvm(411): threadid=1: thread exiting with uncaught exception (group=0x40015560)
11-12 18:15:02.042: E/AndroidRuntime(411): FATAL EXCEPTION: main
11-12 18:15:02.042: E/AndroidRuntime(411): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
11-12 18:15:02.042: E/AndroidRuntime(411): 	at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
11-12 18:15:02.042: E/AndroidRuntime(411): 	at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:470)

En este caso podemos apreciar que cuando la memoria externa ha llegado a 17Mb es cuando el sistema se viene abajo. Te recomiendo que leas mi post sobre gestion de memoria para que entiedas los mensajes. Normalmente es un fallo que salta en aplicaciones con alta carga en imágenes.

Tras probar con cientos de soluciones que he encontrado en la web, llege a la conclusion que ninguna por si sola resolvia mi problema, asi que optimice al maximo mi aplicacion e hice un remix de todas ellas. Asi se soluciono el problema

Solución del problema:

1) En el método onDestroy de cada Activity, es necesirio liberar todos los elementos que se estan utilizando en el sistema. He aqui un ejemplo:

 @Override
protected void onDestroy() {
	    super.onDestroy();
	    this.liberarMemoria();
}
public void liberarMemoria(){
     imagenes.recycleBitmaps();  // <-- esta llamada la voy a explicar a continuación
     this.unbindDrawables(.....vista que deseamos eliminar......);
      //Cerrar cualquier tipo de conexion, webservices, bd, etc...
      //asignar todos los elementos que usamos a NULL
}
 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();
		    }
}

2) El segundo paso es eliminar todas las asignaciones a imágenes como R.drawable.nombreImagen y crearnos un gestor propio de imágenes. Android tiene un bug desde su nacimiento y no gestiona de manera optima las imágenes, en la mayoría de las aplicaciones no hay problema, pero en algunos casos hay que replantearnos la utilización de su gestor.

a) Trasladar todas las imagenes desde la carpeta “\res\drawable-hdpi” a una carpeta que crearemos dentro de assets, tal como “\assets\drawable”

b) Copia esta clase en tu proyecto

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

public class Imagenes {

	  private HashMap> mBitmaps;

          public WeakReference getAssetImage(Context context, String filename){
		//WeakReference tempbitmap = new WeakReference(BitmapFactory.decodeStream(inputStream));
		InputStream is=null;
		AssetManager assets;
		WeakReference bitmap=null;
		try{
			if(mBitmaps==null)mBitmaps=new HashMap>();
			assets = context.getResources().getAssets();
		    is = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
		    bitmap = new WeakReference(BitmapFactory.decodeStream(is));
		    mBitmaps.put(filename,bitmap);
			is.close();
			WeakReference tempDW = new WeakReference(new BitmapDrawable(bitmap.get()));
		    return tempDW;
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			is = null;
			bitmap=null;
		}
		return null;
	}

	public void recycleBitmaps() {
		Iterator itr = mBitmaps.entrySet().iterator();
        while (itr.hasNext()) {
            Map.Entry e = (Map.Entry)itr.next();
            System.out.println("Eliminar:"+e.getKey());
            if(((Bitmap) mBitmaps.get(e.getKey()).get())!=null){
            	((Bitmap) mBitmaps.get(e.getKey()).get()).recycle();
            }
        }
        mBitmaps.clear();
    }

}

c) Cambia todas las llamadas a una imagen

Antes: R.drawable.nombreImagen;

Ahora: Imagenes.getAssetImage(mContext.getApplicationContext(), “nombreImagen”).get();

d) Crea una instancia de Imagenes en cada onCreate de cada Activity

public Imagenes imagenes;
@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		imagenes = new Imagenes(true);

e) La siguiente linea en el AndroidManifest de cada Actividad, mejorara el rendimiento

android:configChanges="orientation|keyboardHidden|navigation"

f) por ultimo optimiza, optimiza y vuelva a optimizar tu codigo

Destacar que bajo mi punto de vista la clave de la solucion es en crear un gestor propio de imagenes que utilice WeakReference

This entry was posted in Android. Bookmark the permalink.

10 Responses to Solución a Exception de Android: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

  1. Arnaldo Gaspar says:

    Llevo un buen rato tratando este problema y la verdad es que logro pequeñas mejoras pero el problema como tal sigue manifestandose. De muchos posts el tuyo es muy esclarecedor y explicativo, voy a intentar tu approach en mi proyecto y te cuento como va. Gracias por compartur tu experiencia.

  2. Raúl says:

    Bueno, yo también llevo ya demasiadas horas con esto (menuda chapuza por parte de Google…), ahora mismo estoy implementando tu solución a ver que tal.

    Solo algunos detalles que le pueden servir a quien quiera probar tu código:
    -En el primer código, sustituir < por simbolo de menor-que
    -En el punto 2a supongo que te refieres a assets[BARRA]drawable
    -En el punto 2c por nombreImagen hay que poner el nombre del archivo sin su extensión ¿no???
    -en Imagenes.getAssetImage(mContext.getApplicationContext(), “nombreImagen”).get(); la “I” de la primera Imagenes ES MINUSCULA

    Por cierto, he tenido que hacer un cast para cambiar del tipo Drawable que devuelve esa sentencia a Bitmap que tenía en mi código, todavía no estoy seguro si he hecho lo correcto:
    Bitmap nod = ((BitmapDrawable)imagenes.getAssetImage(getApplicationContext(), “nod”).get()).getBitmap();

    Bueno, sigo intentandolo, gracias por compartir tu código.

  3. Manuel says:

    Hola, me sucede algo parecido pero todos los enfoques de solucion apuntan a “liberar” una vez cargadas, pero a mi me sucede en el onCreate se cae cuando estoy en pleno proceso de carga de imagenes, asi que por mas que trato de optimizar el cierre no me sirve por que nunca carga… gracias por compartir

  4. eetayo says:

    HOla Manuel,

    perdona por la tardanza de la respuesta. Has probado a utilizar AsyncTask para hacer la carga de imagenes sobre un thread independiente al hilo principal? Has cambiado las imagenes a la carpeta assets?

    Un Saludo!

  5. Pepito says:

    Muchisimas gracias por tu codigo, he tenedio que rematarlo, pero esta perfe.
    Gracias por tu trabajo.

  6. Silvio says:

    Hola, muy bueno el código! Lo voy a probar. Pero tengo una duda, en el punto 1, en la primer línea del método liberarMemoria, la variable imagenes de qué tipo es? Y en la segunda línea: this.unbindDrawables(view), me podrías explicar como funciona y qué vistas le doy como parámetro? Gracias

  7. eetayo says:

    Hola Silvio,
    TIenes la clase completo de Imagenes en el codigo que he puesto, justo aqui “b) Copia esta clase en tu proyecto”
    Sobre la segunda cuestion, debes eliminar todas las view que son de la vista que estas dejando atras antes de hacer el destroy.

    Un Saludo!

  8. Hugo says:

    que tal eetayo tengo un problema similar, pero mi caso es que traigo las imagenes desde un webservice me devuelve un array con las imagenes en un byte[] las cuales correspondientemente las transformot a Bitmap y las plasmo en un Gallery y ImageSwitcher bueno en esta activity tengo pestañas y en las cuales puse su gallery y su imageswitcher en cada pestaña y lo mas seguro es que este muy pesado para cargar todas porque con una si funciona ya con 2 no :/ que me recomendarias
    Saludos.

  9. Eloy Fernández says:

    Hola. Buenos días.
    Estoy haciendo un jueguecito y me está pasando justamente eso. Estoy intentando probar tu código, pero tengo algunas dudas/problemas (voy a ir numerándolos, que creo que será mejor):

    1º)( Cuando instancias la clase
    Imagenes imagenes = new Imagenes(true);
    en el onCreate de la clase activity… no hay constructor de Imagenes que tenga un parámetro booleano, ¿es un error o hay que crear ese constructor? si lo hay que hacer ¿cómo se crea?, es decir, ¿qué significado tiene ese booleano?

    2º) Yo estoy utilizando Drawables en lugar de Bitmap y he hecho esto (pero no estoy muy seguro si dará resultado):
    Drawable draw = (BitmapDrawable) images.getAssetImage(context.getApplicationContext(), "nombreImagen").get();

    3º)(último) En la clase Imagenes haciendo copy paste, han salido tres errores debidos a la asignación por defecto de tipo Object en el Hashmap (Object no tiene el método get), dos se han solucionado parametrizando el Hashmap, pero el otro (en el método getAssetImage), he tenido que hacerle un casting, tal que así:
    WeakReference tempDW = new WeakReference(new BitmapDrawable((Resources) bitmap.get()));
    Esté bien hecho el casting (es el que eclipse ha autogenerado), o hay que hacerlo a (Bitmap). Por cierto, WeakReference ¿se parametriza? ¿con qué tipo de dato?

  10. Pablo says:

    En mi caso he encontrado una solución basada en lo tuyo pero q no es lo mismo exactamente.

    Tengo una AsyncTask corriendo y en un punto dado llamo a la funcion bajarImagen(InputStream is)

    public void bajarImagen(InputStream is) {
    try {
    image = BitmapFactory.decodeStream(is);
    } catch (OutOfMemoryError e) {
    mActivity.limpiar();
    bajarImagen(is);
    }
    }

    Por otra parte en la actividad

    public void limpiar() {
    int cont = 0;
    while(imagenes[cont] == null && cont < num_imagen_actual) cont ++;
    imagenes[cont].recycle();
    imagenes[cont] = null;
    }

    Sobra decir que solo muestro imagen actual, sino petaria el programa, pero de momento me funciona, y no me da problemas para lo que me hace falta

Leave a Reply

Your email address will not be published. Required fields are marked *