Cómo cargar mapas de bits grandes de manera eficiente

Nota: Existen varias bibliotecas que siguen las prácticas recomendadas para subir imágenes. Puedes usar estas bibliotecas en tu app para cargar imágenes de manera optimizada. Te recomendamos la biblioteca Glide, que sube 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, 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 al subir 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 desde varias 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 excepciones de 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 al subir la imagen completa a la memoria.
  • Cantidad de memoria que quieres comprometer para subir esta imagen, dado cualquier otro requisito de memoria de tu aplicación.
  • Dimensiones del componente ImageView o de IU de destino en el que se debe subir 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 en 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. Al subir 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 utilizar 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 a fin de decodificar mapas de bits de otras fuentes, puedes sustituir el método apropiado de BitmapFactory.decode* según sea necesario.