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
}
}