Cómo cargar mapas de bits grandes de manera eficiente

Nota: Existen varias bibliotecas que siguen las prácticas recomendadas para cargar imágenes. Puedes usar estas bibliotecas en tu app para cargar imágenes de manera optimizada. Te recomendamos la biblioteca Glide, que carga y muestra imágenes de la manera más rápida y fluida posible. Otras bibliotecas populares de carga de imágenes son Picasso, de Square, Coil, de Instacart, y Fresco, de Facebook. Estas bibliotecas simplifican la mayoría de las tareas complejas asociadas con mapas de bits y otros tipos de imágenes en Android.

Las imágenes pueden ser de diversas formas y tamaños. En muchos casos, son más grandes de lo necesario para una interfaz de usuario (IU) de aplicación típica. Por ejemplo, la aplicación de la galería del sistema muestra fotos tomadas con la cámara de los dispositivos Android que generalmente tienen una resolución mucho más alta que la densidad de la pantalla del dispositivo.

Dado que estás trabajando con memoria limitada, idealmente solo debes subir una versión de menor resolución a la memoria. La versión de menor resolución debe coincidir con el tamaño del componente de la IU que la muestra. Una imagen con una resolución más alta no proporciona ningún beneficio visible, pero aún ocupa memoria valiosa e incurre en sobrecarga de rendimiento adicional debido a un escalamiento adicional en el momento.

En esta lección, se explica cómo decodificar mapas de bits grandes sin exceder el límite de memoria por aplicación cuando se sube una versión de submuestreo más pequeña a la memoria.

Cómo leer las dimensiones y el tipo del mapa de bits

La clase BitmapFactory proporciona varios métodos de decodificación (decodeByteArray(), decodeFile(), decodeResource(), etc.) para crear un Bitmap de diferentes fuentes. Elige el método de decodificación más adecuado según tu fuente de datos de imágenes. Estos métodos intentan asignar memoria para el mapa de bits construido y, por lo tanto, pueden generar fácilmente una excepción OutOfMemory. Cada tipo de método de decodificación tiene firmas adicionales que te permiten especificar opciones de decodificación a través de la clase BitmapFactory.Options. Al establecer la propiedad inJustDecodeBounds en true durante la decodificación, se evita la asignación de memoria, con lo cual se muestra null para el objeto de mapa de bits, pero se establecen outWidth, outHeight y outMimeType. Esta técnica permite leer las dimensiones y el tipo de los datos de la imagen antes de la construcción (y la asignación de memoria) del mapa de bits.

Kotlin

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

Java

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 las excepciones java.lang.OutOfMemory, comprueba las dimensiones de un mapa de bits antes de decodificarlo, a menos que confíes absolutamente en que la fuente te proporcionará datos de imagen de tamaño predecible que se ajusten cómodamente a la memoria disponible.

Cómo subir una versión reducida a la memoria

Ahora que se conocen las dimensiones de la imagen, se pueden usar para decidir si la imagen completa se debe subir en la memoria o si se debe subir una versión de submuestreo. Estos son algunos factores para considerar:

  • Uso de memoria estimado cuando se sube la imagen completa a la memoria
  • Cantidad de memoria que quieres comprometer para cargar esta imagen, dado cualquier otro requisito de memoria de tu aplicación
  • Dimensiones del componente ImageView o de IU objetivo en el que se debe cargar la imagen
  • Tamaño de pantalla y densidad del dispositivo actual

Por ejemplo, no vale la pena cargar una imagen de 1024 x 768 píxeles en la memoria si finalmente se mostrará en una miniatura de 128 x 96 píxeles en ImageView.

Para indicar al decodificador que debe realizar un submuestreo de la imagen y subir una versión más pequeña a la memoria, establece inSampleSize como true en tu objeto BitmapFactory.Options. Por ejemplo, una imagen con una resolución de 2048 x 1536 que se decodifica con un inSampleSize de 4 produce un mapa de bits de aproximadamente 512 x 384. Cuando se carga esto en la memoria, se utilizan 0.75 MB en lugar de 12 MB para la imagen completa (suponiendo una configuración de mapa de bits de ARGB_8888). Aquí se muestra un método para calcular un valor de tamaño de muestra con la potencia de dos en función del ancho y la altura del objetivo:

Kotlin

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

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

        val halfHeight: Int = height / 2
        val halfWidth: Int = 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
}

Java

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 una potencia de dos valores porque el decodificador utiliza un valor final redondeando a la potencia de dos valores más cercana y más baja, según la documentación de inSampleSize.

Para usar este método, primero decodifica con inJustDecodeBounds establecido en true, pasa las opciones y, luego, decodifica de nuevo utilizando el nuevo valor inSampleSize y inJustDecodeBounds establecido en false:

Kotlin

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}

Java

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 ImageView que muestra una miniatura de 100 x 100 píxeles, como se muestra en el siguiente código de ejemplo:

Kotlin

imageView.setImageBitmap(
        decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

Java

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

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