Al reproducir secuancias de fotogramas con una gran cantidad de imágenes, es común encontrar excepciones de OutOfMemoryError (OOM).
Análisis del Problema
El código original creaba una nueva instancia de Bitmap para cada fotograma, lo que resultaba en un consumo de memoria excesivo. Aunque se utilizaba bitmap.recycle(), este método no libera la memoria de inmediato, lo que sigue causando problemas de memoria insuficiente.
Solución Propuesta
La solución consiste en aprovechar la opción inBitmap de BitmapFactory.Options. Esto permite reutilizar la misma región de memoria para cada fotograma, reduciendo significativamente la presión sobre la memoria. Se inicializa un Bitmap base con las dimensiones y configuración deseadas, y luego se asigna a inBitmap. Al decodificar los recursos de imagen subsiguientes, BitmapFactory intentará reutilizar esta área de memoria.
A continuación, se muestra un fragmento de código que ilustra cómo configurar la reutilización de memoria:
// Configura la reutilización de memoria del Bitmap
mBitmapOptions.inBitmap = mBitmap; // Reutiliza el bloque de memoria del Bitmap, similar a un pool de objetos, para evitar asignaciones de memoria innecesarias.
// Inicializa el bitmap usando estos parámetros
bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);
Implementación Completa
La siguiente clase encapsula la lógica para la animación de secuencia de fotogramas con optimización de memoria:
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Handler;
import android.widget.ImageView;
import java.lang.ref.SoftReference;
/**
* Clase para la reproducción de animaciones de secuencia de fotogramas.
* Implementa la reutilización de memoria de Bitmap para prevenir OOM.
*/
public class FramesSequenceAnimation {
private final int[] mFrames; // Array de recursos de imagen para los fotogramas.
private int mCurrentFrameIndex; // Índice del fotograma actual.
private boolean mShouldPlay; // Indica si la animación debe continuar reproduciéndose.
private boolean mIsPlaying; // Indica si la animación está actualmente en reproducción.
private SoftReference<imageview> mImageViewRef; // Referencia suave al ImageView para permitir la recolección de basura.
private Handler mHandler; // Handler para programar la siguiente actualización del fotograma.
private int mFrameDelayMillis; // Retardo entre fotogramas en milisegundos.
private OnAnimationStoppedListener mOnAnimationStoppedListener; // Listener para notificar cuando la animación se detiene.
private Bitmap mReusableBitmap = null; // Bitmap reutilizable para optimizar la memoria.
private BitmapFactory.Options mBitmapOptions; // Opciones de BitmapFactory para gestionar la decodificación de bitmaps.
/**
* Constructor para FramesSequenceAnimation.
* @param imageView El ImageView donde se mostrará la animación.
* @param frames Array de IDs de recursos de imagen que componen la animación.
* @param fps Fotogramas por segundo deseados para la animación.
*/
public FramesSequenceAnimation(ImageView imageView, int[] frames, int fps) {
mHandler = new Handler();
mFrames = frames;
mCurrentFrameIndex = -1;
mImageViewRef = new SoftReference<>(imageView);
mShouldPlay = false;
mIsPlaying = false;
mFrameDelayMillis = 1000 / fps; // Calcula el retardo entre fotogramas.
// Establece el primer fotograma inicialmente.
imageView.setImageResource(mFrames[0]);
// Inicializa el bitmap reutilizable si la versión de Android lo soporta.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
Bitmap initialBitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
int width = initialBitmap.getWidth();
int height = initialBitmap.getHeight();
Bitmap.Config config = initialBitmap.getConfig();
// Crea un bitmap base para la reutilización.
mReusableBitmap = Bitmap.createBitmap(width, height, config);
mBitmapOptions = new BitmapFactory.Options();
// Configura las opciones para la reutilización de memoria.
mBitmapOptions.inBitmap = mReusableBitmap;
mBitmapOptions.inMutable = true; // Permite la modificación del bitmap decodificado.
mBitmapOptions.inSampleSize = 1; // Factor de escala inicial.
}
}
/**
* Obtiene el índice del siguiente fotograma en la secuencia.
* Si se alcanza el final, reinicia al principio.
* @return El ID del recurso del siguiente fotograma.
*/
private int getNextFrameResource() {
mCurrentFrameIndex++;
if (mCurrentFrameIndex >= mFrames.length) {
mCurrentFrameIndex = 0; // Reinicia el ciclo.
}
return mFrames[mCurrentFrameIndex];
}
/**
* Inicia la reproducción de la animación.
* Utiliza sincronización para prevenir accesos concurrentes peligrosos.
*/
public synchronized void start() {
mShouldPlay = true;
if (mIsPlaying) {
return; // Ya está reproduciendo, no hacer nada.
}
final Runnable frameUpdater = new Runnable() {
@Override
public void run() {
ImageView imageView = mImageViewRef.get();
// Detiene la reproducción si no se debe seguir o si el ImageView ha sido liberado.
if (!mShouldPlay || imageView == null) {
mIsPlaying = false;
if (mOnAnimationStoppedListener != null) {
mOnAnimationStoppedListener.onAnimationStopped();
}
return;
}
mIsPlaying = true;
// Programa la actualización del siguiente fotograma.
mHandler.postDelayed(this, mFrameDelayMillis);
// Actualiza el fotograma solo si el ImageView está visible.
if (imageView.isShown()) {
int nextFrameResId = getNextFrameResource();
if (mReusableBitmap != null) { // Si se está utilizando la optimización de memoria.
Bitmap decodedBitmap = null;
try {
// Intenta decodificar el recurso utilizando el bitmap reutilizable.
decodedBitmap = BitmapFactory.decodeResource(imageView.getResources(), nextFrameResId, mBitmapOptions);
} catch (IllegalArgumentException e) {
// Si la reutilización falla (ej. diferente tamaño o formato),
// decodifica normalmente y libera el bitmap reutilizable.
System.err("Bitmap reuse failed, falling back to standard decode. Error: " + e.getMessage());
imageView.setImageResource(nextFrameResId);
mReusableBitmap.recycle();
mReusableBitmap = null; // Desactiva la reutilización para futuras decodificaciones.
return; // Sale del método para evitar usar decodedBitmap.
} catch (Exception e) {
e.printStackTrace();
}
if (decodedBitmap != null) {
// Si la decodificación fue exitosa con reutilización, establece el nuevo bitmap.
imageView.setImageBitmap(decodedBitmap);
} else {
// Si la decodificación falló por alguna razón inesperada, vuelve asetImageResource.
imageView.setImageResource(nextFrameResId);
// Podría ser necesario manejar el caso donde mReusableBitmap se vuelve inválido.
if (mReusableBitmap != null) {
mReusableBitmap.recycle();
mReusableBitmap = null;
}
}
} else {
// Si no se está utilizando la optimización (versiones antiguas o fallo previo), usa el método estándar.
imageView.setImageResource(nextFrameResId);
}
}
}
};
mHandler.post(frameUpdater); // Inicia el ciclo de actualización.
}
/**
* Detiene la reproducción de la animación.
* Elimina todas las llamadas programadas en el Handler.
*/
public synchronized void stop() {
mShouldPlay = false;
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null); // Elimina todas las callbacks y mensajes pendientes.
}
// Libera el bitmap reutilizable al detener la animación.
if (mReusableBitmap != null) {
mReusableBitmap.recycle();
mReusableBitmap = null;
}
}
/**
* Interfaz para escuchar el evento de fin de animación.
*/
public interface OnAnimationStoppedListener {
void onAnimationStopped();
}
/**
* Establece un listener para ser notificado cuando la animación se detenga.
* @param listener El listener a establecer.
*/
public void setOnAnimationStoppedListener(OnAnimationStoppedListener listener) {
this.mOnAnimationStoppedListener = listener;
}
}
</imageview>