Optimización de Carga de Imágenes en Android: Análisis de BitmapFactory.decodeFile vs ImageDecoder.decodeBitmap

La carga y decodificación eficiente de imágenes es un aspecto crítico en el desarrollo de aplicaciones Android, impactando directamente el rendimiento y la experiencia de usuario. Android ofrece diversas APIs para manejar este proceso, siendo BitmapFactory una herramienta tradicional y ImageDecoder una alternativa moderna introducida a partir de Android P (API 28) que busca mejorar la flexibilidad y eficiencia.

Este artículo compara el rendimiento y la facilidad de uso de ambas aproximaciones al decodificar archivos de imagen y video desde el almacenamiento local, utilizando un ejemplo práctico en Kotlin.

Permisos Necesarios

Para acceder a los archivos multimedia almacenados en el dispositivo, es fundamental declarar los perimsos adecuados en el archivo AndroidManifest.xml. A partir de Android 10 (API 29), los permisos específicos para medios como READ_MEDIA_IMAGES y READ_MEDIA_VIDEO son preferibles a READ_EXTERNAL_STORAGE para un control más granular.


   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
   <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
   

Implementación y Comparativa

El siguiente código Kotlin implementa una actividad que lee una selección de imágenes y videos del dispositivo, y luego compara el tiempo y la tasa de éxito al decodificar estos archivos usando BitmapFactory.decodeFile y ImageDecoder.decodeBitmap.


   import android.content.ContentUris
   import android.content.Context
   import android.graphics.Bitmap
   import android.graphics.BitmapFactory
   import android.graphics.ImageDecoder
   import android.net.Uri
   import android.os.Bundle
   import android.provider.MediaStore
   import android.util.Log
   import androidx.appcompat.app.AppCompatActivity
   import androidx.lifecycle.lifecycleScope
   import kotlinx.coroutines.Dispatchers
   import kotlinx.coroutines.launch
   import java.io.File

   class AnalisisDecodificacionActivity : AppCompatActivity() {

       companion object {
           const val ETIQUETA_LOG = "AppDecoder"
           const val DIMENSION_OBJETIVO = 400
           const val LIMITE_ARCHIVOS_PRUEBA = 3000
       }

       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           // No se establece un layout visual para esta actividad de prueba
           // La lógica se ejecuta en segundo plano.

           val contextoApp = this
           lifecycleScope.launch(Dispatchers.IO) {
               val listaImagenes = obtenerTodasLasImagenes(contextoApp)
               val listaVideos = obtenerTodosLosVideos(contextoApp)

               Log.d(ETIQUETA_LOG, "Imágenes encontradas: ${listaImagenes.size}")
               Log.d(ETIQUETA_LOG, "Videos encontrados: ${listaVideos.size}")

               val recursosMultimedia = ArrayList<ItemMultimedia>().apply {
                   addAll(listaImagenes)
                   addAll(listaVideos)
                   shuffle() // Mezclar para una distribución aleatoria
               }

               ejecutarPruebaDecodificacion(recursosMultimedia.take(LIMITE_ARCHIVOS_PRUEBA))
           }
       }

       private suspend fun ejecutarPruebaDecodificacion(listaRecursos: List<ItemMultimedia>) {
           var fallosBitmapFactory = 0
           var tiempoInicio = System.currentTimeMillis()

           listaRecursos.forEach { item ->
               val bitmapDecodificado = decodificarConBitmapFactory(item.rutaArchivo)
               if (bitmapDecodificado == null) {
                   fallosBitmapFactory++
               } else {
                   bitmapDecodificado.recycle() // Liberar memoria
               }
           }
           val duracionBitmapFactory = System.currentTimeMillis() - tiempoInicio

           var fallosImageDecoder = 0
           tiempoInicio = System.currentTimeMillis()

           listaRecursos.forEach { item ->
               try {
                   val bitmapDecodificado = decodificarConImageDecoder(item.rutaArchivo)
                   bitmapDecodificado?.recycle() // Liberar memoria
               } catch (e: Exception) {
                   fallosImageDecoder++
                   Log.e(ETIQUETA_LOG, "Error decodificando con ImageDecoder: ${e.message}")
               }
           }
           val duracionImageDecoder = System.currentTimeMillis() - tiempoInicio

           Log.d(ETIQUETA_LOG, "--- Resultados de Decodificación ---")
           Log.d(ETIQUETA_LOG, "BitmapFactory: Duración = ${duracionBitmapFactory}ms, Errores = $fallosBitmapFactory")
           Log.d(ETIQUETA_LOG, "ImageDecoder: Duración = ${duracionImageDecoder}ms, Errores = $fallosImageDecoder")
       }

       /**
        * Decodifica un archivo de imagen utilizando BitmapFactory.decodeFile.
        * Configura el tamaño de salida y el formato de píxeles.
        */
       private fun decodificarConBitmapFactory(rutaArchivo: String): Bitmap? {
           val opcionesDecodificacion = BitmapFactory.Options().apply {
               outWidth = DIMENSION_OBJETIVO
               outHeight = DIMENSION_OBJETIVO
               inPreferredConfig = Bitmap.Config.ARGB_8888
               inJustDecodeBounds = true // Obtener solo las dimensiones sin cargar en memoria
               BitmapFactory.decodeFile(rutaArchivo, this)

               // Calcular inSampleSize para reducir la resolución
               inSampleSize = calculateInSampleSize(this, DIMENSION_OBJETIVO, DIMENSION_OBJETIVO)
               inJustDecodeBounds = false // Decodificar el bitmap real
           }
           return BitmapFactory.decodeFile(rutaArchivo, opcionesDecodificacion)
       }

       /**
        * Calcula el valor de inSampleSize para BitmapFactory.
        */
       private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
           val (height, width) = 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
       }

       /**
        * Decodifica un archivo de imagen utilizando ImageDecoder.decodeBitmap.
        * Permite establecer un tamaño objetivo y otras propiedades de decodificación.
        */
       private fun decodificarConImageDecoder(rutaArchivo: String): Bitmap? {
           val fuenteImagen = ImageDecoder.createSource(File(rutaArchivo))
           return ImageDecoder.decodeBitmap(fuenteImagen) { decodificador, informacion, fuente ->
               decodificador.setTargetSize(DIMENSION_OBJETIVO, DIMENSION_OBJETIVO)
               decodificador.isMutableRequired = false // Si no se necesita un bitmap mutable
           }
       }

       /**
        * Clase de datos simple para almacenar la ruta y URI de un recurso multimedia.
        */
       data class ItemMultimedia(val rutaArchivo: String, val uriContenido: Uri)

       /**
        * Obtiene una lista de todas las imágenes almacenadas externamente.
        */
       private fun obtenerTodasLasImagenes(contexto: Context): ArrayList<ItemMultimedia> {
           val listaResultadosImagenes = ArrayList<ItemMultimedia>()
           val proyeccion = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA)
           val uriColeccion = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

           contexto.contentResolver.query(
               uriColeccion,
               proyeccion,
               null,
               null,
               "${MediaStore.Images.Media.DATE_ADDED} DESC" // Ordenar por fecha de adición
           )?.use { cursor ->
               val columnaRuta = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
               val columnaId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)

               while (cursor.moveToNext()) {
                   val ruta = cursor.getString(columnaRuta)
                   val idImagen = cursor.getLong(columnaId)
                   val uriImagen = ContentUris.withAppendedId(uriColeccion, idImagen)
                   listaResultadosImagenes.add(ItemMultimedia(ruta, uriImagen))
               }
           }
           return listaResultadosImagenes
       }

       /**
        * Obtiene una lista de todos los videos almacenados externamente.
        */
       private fun obtenerTodosLosVideos(contexto: Context): ArrayList<ItemMultimedia> {
           val listaResultadosVideos = ArrayList<ItemMultimedia>()
           val proyeccion = arrayOf(MediaStore.Video.Media._ID, MediaStore.Video.Media.DATA)
           val uriColeccion = MediaStore.Video.Media.EXTERNAL_CONTENT_URI

           contexto.contentResolver.query(
               uriColeccion,
               proyeccion,
               null,
               null,
               "${MediaStore.Video.Media.DATE_ADDED} DESC"
           )?.use { cursor ->
               val columnaRuta = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
               val columnaId = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)

               while (cursor.moveToNext()) {
                   val ruta = cursor.getString(columnaRuta)
                   val idVideo = cursor.getLong(columnaId)
                   val uriVideo = ContentUris.withAppendedId(uriColeccion, idVideo)
                   listaResultadosVideos.add(ItemMultimedia(ruta, uriVideo))
               }
           }
           return listaResultadosVideos
       }
   }
   

Etiquetas: Android Kotlin BitmapFactory ImageDecoder ImageProcessing

Publicado el 7-3 04:30