Extraño problema de OutOfMemory al cargar una imagen en un objeto de mapa de bits
Tengo un ListView
con 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 ListView
actividad 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 ListView
se 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 src
botó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 OutOfMemoryError
cuando 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, ListView
volvió 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.imagefilename
está 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
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 inSampleSize
opció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 inSampleSize
valor, 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;
}
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 BitmapFactory
clase 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()
Bitmap
OutOfMemory
BitmapFactory.Options
inJustDecodeBounds
true
null
outWidth
outHeight
outMimeType
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.OutOfMemory
excepciones, 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 inSampleSize
en true
su BitmapFactory.Options
objeto. Por ejemplo, una imagen con resolución 2048x1536 que se decodifica con un valor inSampleSize
de 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
inSampleSize
documentación.
Para utilizar este método, primero decodifique con inJustDecodeBounds
el valor true, pass the options through and then decode again using the new
inSampleSize value and
inJustDecodeBounds set to
false`:
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 ImageView
que 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.
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;
}
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.Options
with inPurgeable
set to true
(y preferiblemente con inInputShareable
tambié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).
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);
}