Buenas practicas con Bitmap

Iniciado por Cronos, Febrero 17, 2015, 11:57:46 AM

Tema anterior - Siguiente tema

0 Miembros y 1 Visitante están viendo este tema.

Febrero 17, 2015, 11:57:46 AM Ultima modificación: Abril 17, 2015, 02:05:34 PM por ANTRAX
Buenas practicas con Bitmap

Cargando grandes mapas de bits de manera eficiente
Muchas veces tratamos de cargar una imagen de tamaño muy grande en un espacio muy pequeño, y esto funciona, pero no de la manera que esperamos. Al crear un listView con una colección de Bitmap mal implementado, nos daremos cuenta que al deslizar nuestra lista, esta se torna pesada y torpe. Esto es por el tamaño de las imágenes que hemos cargado en cada Bitmap.
Por ejemplo, no vale la pena cargar una imagen de 1024x768 píxeles en la memoria si se mostrará en una miniatura de 128x96 píxeles en un imageView. La mejor practica para evitar esto es reducir el tamaño de imagen que se va a crear en el Bitmap al que se calcula que uno va a usar. Para esto utilizamos la opción inSampleSize, esta opción lo que hace es indica la escala que se utilizara para reducir dicha imagen. Por ejemplo, una imagen con resolución 2048x1536 que se cargara con un inSampleSize de 4 produce un mapa de bits de aproximadamente 512x384, cargando esto en memoria utiliza 0.75MB en lugar de 12 MB para la imagen completa.
Una pregunta común es, como saber que valor al inSampleSize debo colocar? Pues hay un método para calcular dicho tamaño de manera automática, que es el siguiente.

Código: text

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    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;
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}


Este método recibe como parámetros el elemento options que utilizaremos para crear nuestro Bitmap, el Width y Height que deseamos que tenga nuestro Bitmap y devuelve un valor de tipo int el cual sera nuestro valor a colocar en inSampleSize para obtener este tamaño deseado.
Para utilizar este método, primero debemos decodificar con la opción inJustDecodeBounds establecida en true, se pasan el options y luego decodificar de nuevo usando el nuevo valor de  inSampleSize y inJustDecodeBounds establecido esta ves en false.

Código: text

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 hace que sea fácil de cargar un mapa de bits de tamaño arbitrariamente grande en un ImageView que muestra una miniatura de 100x100 píxeles, como se muestra en el siguiente código de ejemplo:

Código: text

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


Ahora bien una ves implementado esto en el caso que dijimos anteriormente se puede notar una gran diferente la fluidez al desplazar nuestra lista, pero esto no queda acá. Las personas que no nos conformamos con poco notaremos que aun la lista de desplaza con cierto retraso que no es nada agradable a la vista. Esto sucede porque cada ves que deslizamos y mostramos los items ocultos en la lista se vuelve a calcular y a cargar la imagen en los Bitmap y todo este proceso se produce en el hilo principal, lo cual tenemos que evitar, entonces el siguiente paso es hacer este proceso de cargado en un hilo secundario, para ello utilizaremos AsyncTask.

Procesando Bitmap en un hilo secundario
La clase AsyncTask proporciona una manera fácil de ejecutar algún trabajo en un subproceso de fondo y publicar los resultados de vuelta en el subproceso de interfaz de usuario. Para usarlo, se crea una subclase y se sobrescriben los métodos proporcionados. Un ejemplo de la carga de una imagen de gran tamaño en un imageView usando AsyncTask y el método creado anteriormente  decodeSampledBitmapFromResource ():

Código: text

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;
    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }
    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}


Podemos observar que el constructor recibe un imageView el cual sera el que utilizaremos para cargar el Bitmap, el método doInBackground() ejecuta el método decodeSampledBitmapFromResource() en segundo plano y retorna un Bitmap el cual lo va a recibir el método onPostExecute() el cual se encargar de plasmar los resultados en la interfaz principal.
Para comenzar a cargar el mapa de bits de forma asíncrona, basta con crear una nueva tarea y ejecutarlo:

Código: text

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}


Observamos que la ejecutar la tarea en segundo plano se le pasa una variable resId, el cual es el id con la ruta de la imagen a cagar.

Almacenando un Bitmap en cache
Al ver los resultados que nos arroja el paso anterior podemos observar que el deslizamiento de nuestra lista es muy veloz, pero que tiene un pequeño problema. Las imágenes tardan en cargarse, y esto es algo esperado ya que el proceso de cargar los item de la lista en el hilo principal no van a esperar ah que termine el proceso en el hilo secundario, entonces se formara una desincronización a la hora de cargar los datos de cada item, por mas que simplemente sean microsegundos, esto es algo que se nota notablemente en la UI e irritaría mucho al usuario.
Entonces como solucionamos este problema? Cargando cada Bitmap en cache.
Una memoria caché ofrece un acceso rápido a los Bitmap a costa de ocupar memoria valiosa de la aplicación. La clase LruCache esta especialmente adecuado para la tarea de Bitmap en caché.
Con el fin de elegir un tamaño adecuado para el LruCache, un número de factores deben ser tomados en consideración, por ejemplo:

-  ¿Cuántas imágenes estará en pantalla a la vez? ¿Cuántos tienen que estar disponibles listos para entrar en la pantalla?
-  ¿Cuál es el tamaño de la pantalla y la densidad del dispositivo? Un dispositivo de alta densidad (xhdpi) como Galaxy Nexus tendrá un caché más grande para mantener el mismo número de imágenes en la memoria en comparación con un dispositivo como Nexus S (IPAP).
-  ¿Con qué frecuencia se accederá a las imágenes? ¿Algunos acceder con mayor frecuencia que los demás? Si es así, tal vez usted puede querer mantener ciertos artículos siempre en la memoria o incluso tener varios objetos LruCache para diferentes grupos de mapas de bits.
-  ¿Puede equilibrar la calidad contra cantidad? A veces puede ser más útil para almacenar un mayor número de Bitmap de menor calidad.

No hay un tamaño específico o fórmula que se adapte a todas las aplicaciones, es cuestión de cada uno analizar su consumo y llegar a una solución adecuada. Una caché que es demasiado pequeña causa sobrecarga adicional sin ningún beneficio, una memoria caché que es demasiado grande  puede devolver excepciones java.lang.OutOfMemory y dejar al resto de su aplicación poca memoria para trabajar. Un ejemplo de la creación de un LruCache para Bitmap:

Código: text

private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}


Ahora cuando vallamos a cargar un Bitmap en un imageView, primero comprobaremos que este Bitmap no esta ya cargado en cache. En caso de que este en cache, evitaremos todo el proceso de redimensionamiento echo anteriormente y simplemente lo traeremos de la cache, caso contrario haremos los pasos mencionados anteriormente y almacenaremos ese Bitmap en cache si no existe, para que la próxima ves que tengamos que cargarlo, lo traigamos de cache.

Código: text

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}


En la clase BitmapWorkerTask creada anteriormente solo debemos agregar la linea de código que se encarga de cargar este nuevo Bitmap en cache.

Código: text

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}


Y eso seria todo. Ya tenemos una lista que carga por única vez los Bitmap y luego hace petición a cache, la eficiencia de esto es muy grande y los resultados son muy notorios.
Para realizar este manual me base en la información oficial de Google la cual colocare a continuación.

No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
No tienes permitido ver los links. Registrarse o Entrar a mi cuenta
No tienes permitido ver los links. Registrarse o Entrar a mi cuenta

Autor: Leandro Vitale

Saludos,, Cronos.-