Extraño problema de OutOfMemory al cargar una imagen en un objeto de mapa de bits

Resuelto Chrispix asked hace 54 años • 44 respuestas

Tengo un ListViewcon un par de botones de imagen en cada fila. Cuando el usuario hace clic en la fila de la lista, inicia una nueva actividad. Tuve que crear mis propias pestañas debido a un problema con el diseño de la cámara. La actividad que se inicia para obtener el resultado es un mapa. Si hago clic en mi botón para iniciar la vista previa de la imagen (cargar una imagen de la tarjeta SD), la aplicación regresa de la actividad a la ListViewactividad y al controlador de resultados para reiniciar mi nueva actividad, que no es más que un widget de imagen.

La vista previa de la imagen en el ListViewse realiza con el cursor y ListAdapter. Esto lo hace bastante simple, pero no estoy seguro de cómo puedo poner una imagen redimensionada (es decir, un tamaño de bit más pequeño, no un píxel como el del srcbotón de imagen sobre la marcha. Así que simplemente cambié el tamaño de la imagen que salió de la cámara del teléfono.

El problema es que recibo un mensaje OutOfMemoryErrorcuando intento regresar y reiniciar la segunda actividad.

  • ¿Hay alguna manera de crear el adaptador de lista fácilmente fila por fila, donde pueda cambiar el tamaño sobre la marcha ( bit a bit )?

Esto sería preferible ya que también necesito realizar algunos cambios en las propiedades de los widgets/elementos en cada fila, ya que no puedo seleccionar una fila con la pantalla táctil debido al problema de enfoque. ( Puedo usar rollerball. )

  • Sé que puedo cambiar el tamaño fuera de banda y guardar mi imagen, pero eso no es realmente lo que quiero hacer, pero estaría bien algún código de muestra para eso.

Tan pronto como desactivé la imagen, ListViewvolvió a funcionar bien.

FYI: Así es como lo estaba haciendo:

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME, DBHelper.KEY_ADDRESS,
    DBHelper.KEY_CITY, DBHelper.KEY_GPSLONG, DBHelper.KEY_GPSLAT,
    DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] { R.id.businessname, R.id.address, R.id.city, R.id.gpslong,
    R.id.gpslat, R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

Dónde R.id.imagefilenameestá un ButtonImage.

Aquí está mi LogCat:

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

También tengo un nuevo error al mostrar una imagen:

22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed
Chrispix avatar Jan 01 '70 08:01 Chrispix
Aceptado

Para corregir el error OutOfMemory, debes hacer algo como esto:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap preview_bitmap = BitmapFactory.decodeStream(is, null, options);

Esta inSampleSizeopción reduce el consumo de memoria.

Aquí tienes un método completo. Primero lee el tamaño de la imagen sin decodificar el contenido en sí. Luego encuentra el mejor inSampleSizevalor, debe ser una potencia de 2, y finalmente se decodifica la imagen.

// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE=70;

        // Find the correct scale value. It should be the power of 2.
        int scale = 1;
        while(o.outWidth / scale / 2 >= REQUIRED_SIZE && 
              o.outHeight / scale / 2 >= REQUIRED_SIZE) {
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (FileNotFoundException e) {}
    return null;
}
Fedor avatar May 05 '2009 09:05 Fedor

La clase de capacitación de Android , " Visualización de mapas de bits de manera eficiente ", ofrece excelente información para comprender y abordar la excepción `java.lang.OutOfMemoryError: el tamaño del mapa de bits excede el presupuesto de la VM al cargar mapas de bits.


Leer dimensiones y tipo de mapa de bits

La BitmapFactoryclase proporciona varios métodos de decodificación ( ,,, decodeByteArray()etc. ) para crear archivos a partir de varias fuentes. Elija el método de decodificación más apropiado según la fuente de datos de su imagen. Estos métodos intentan asignar memoria para el mapa de bits construido y, por lo tanto, pueden generar fácilmente una excepción. Cada tipo de método de decodificación tiene firmas adicionales que le permiten especificar opciones de decodificación a través de la clase. Establecer la propiedad en durante la decodificación evita la asignación de memoria, regresando para el objeto de mapa de bits pero configurando y . Esta técnica le permite leer las dimensiones y el tipo de datos de la imagen antes de la construcción (y asignación de memoria) del mapa de bits.decodeFile()decodeResource()BitmapOutOfMemoryBitmapFactory.OptionsinJustDecodeBoundstruenulloutWidthoutHeightoutMimeType

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

Para evitar java.lang.OutOfMemoryexcepciones, verifique las dimensiones de un mapa de bits antes de decodificarlo, a menos que confíe absolutamente en la fuente para proporcionarle datos de imagen de tamaño predecible que se ajusten cómodamente a la memoria disponible.


Cargue una versión reducida en la memoria

Ahora que se conocen las dimensiones de la imagen, se pueden utilizar para decidir si se debe cargar la imagen completa en la memoria o si se debe cargar una versión submuestreada. Aquí hay algunos factores a considerar:

  • Uso estimado de memoria al cargar la imagen completa en la memoria.
  • La cantidad de memoria que está dispuesto a dedicar para cargar esta imagen teniendo en cuenta cualquier otro requisito de memoria de su aplicación.
  • Dimensiones del componente ImageView o UI de destino en el que se cargará la imagen.
  • Tamaño de pantalla y densidad del dispositivo actual.

Por ejemplo, no vale la pena cargar una imagen de 1024x768 píxeles en la memoria si eventualmente se mostrará en una miniatura de 128x96 píxeles en un archivo ImageView.

Para indicarle al decodificador que submuestree la imagen, cargue una versión más pequeña en la memoria, configurada inSampleSizeen truesu BitmapFactory.Optionsobjeto. Por ejemplo, una imagen con resolución 2048x1536 que se decodifica con un valor inSampleSizede 4 produce un mapa de bits de aproximadamente 512x384. Cargar esto en la memoria utiliza 0,75 MB en lugar de 12 MB para la imagen completa (suponiendo una configuración de mapa de bits de ARGB_8888). A continuación se muestra un método para calcular un valor de tamaño de muestra que es una potencia de dos en función del ancho y alto del objetivo:

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Nota : Se calcula un valor de potencia de dos porque el decodificador utiliza un valor final redondeando hacia abajo a la potencia de dos más cercana, según la inSampleSizedocumentación.

Para utilizar este método, primero decodifique con inJustDecodeBoundsel valor true, pass the options through and then decode again using the new inSampleSize value andinJustDecodeBounds set tofalse`:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
    int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Este método facilita la carga de un mapa de bits de tamaño arbitrariamente grande en un archivo ImageViewque muestra una miniatura de 100x100 píxeles, como se muestra en el siguiente código de ejemplo:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

Puede seguir un proceso similar para decodificar mapas de bits de otras fuentes, sustituyendo el BitmapFactory.decode*método apropiado según sea necesario.

Sazid avatar Apr 12 '2012 16:04 Sazid

Hice una pequeña mejora en el código de Fedor. Básicamente hace lo mismo, pero sin el feo bucle while (en mi opinión) y siempre resulta en una potencia de dos. Felicitaciones a Fedor por crear la solución original, estuve estancado hasta que encontré la suya y luego pude hacer esta :)

 private Bitmap decodeFile(File f){
    Bitmap b = null;

        //Decode image size
    BitmapFactory.Options o = new BitmapFactory.Options();
    o.inJustDecodeBounds = true;

    FileInputStream fis = new FileInputStream(f);
    BitmapFactory.decodeStream(fis, null, o);
    fis.close();

    int scale = 1;
    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
        scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / 
           (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
    }

    //Decode with inSampleSize
    BitmapFactory.Options o2 = new BitmapFactory.Options();
    o2.inSampleSize = scale;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, o2);
    fis.close();

    return b;
}
Thomas Vervest avatar Aug 23 '2010 15:08 Thomas Vervest

Vengo de experiencia en iOS y me sentí frustrado al descubrir un problema con algo tan básico como cargar y mostrar una imagen. Después de todo, todos los que tienen este problema intentan mostrar imágenes de tamaño razonable. De todos modos, aquí están los dos cambios que solucionaron mi problema (e hicieron que mi aplicación respondiera muy bien).

1) Cada vez que lo hagas BitmapFactory.decodeXYZ(), asegúrate de pasar BitmapFactory.Optionswith inPurgeableset to true(y preferiblemente con inInputShareabletambién set to true).

2) NUNCA lo use Bitmap.createBitmap(width, height, Config.ARGB_8888). ¡Quiero decir NUNCA! Nunca he tenido esa cosa que no genere un error de memoria después de algunas pasadas. Ninguna cantidad de recycle()lo System.gc()que sea ayudó. Siempre planteó excepción. La otra forma que realmente funciona es tener una imagen ficticia en tus elementos de diseño (u otro mapa de bits que hayas decodificado usando el paso 1 anterior), reescalarlo a lo que quieras y luego manipular el mapa de bits resultante (como pasarlo a un lienzo). para más diversión). Entonces, lo que deberías usar en su lugar es: Bitmap.createScaledBitmap(srcBitmap, width, height, false). Si por alguna razón DEBE utilizar el método de creación de fuerza bruta, al menos pase Config.ARGB_4444.

Es casi seguro que esto le ahorrará horas, si no días. Todo eso sobre escalar la imagen, etc., realmente no funciona (a menos que considere como solución obtener un tamaño incorrecto o una imagen degradada).

Ephraim avatar Dec 15 '2011 22:12 Ephraim

Es un error conocido , no se debe a archivos grandes. Dado que Android almacena en caché los elementos dibujables, se queda sin memoria después de usar algunas imágenes. Pero encontré una forma alternativa de hacerlo, omitiendo el sistema de caché predeterminado de Android.

Solución : mueva las imágenes a la carpeta "activos" y use la siguiente función para obtener BitmapDrawable:

public static Drawable getAssetImage(Context context, String filename) throws IOException {
    AssetManager assets = context.getResources().getAssets();
    InputStream buffer = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
    Bitmap bitmap = BitmapFactory.decodeStream(buffer);
    return new BitmapDrawable(context.getResources(), bitmap);
}
Anto Binish Kaspar avatar May 24 '2011 20:05 Anto Binish Kaspar