Saturday, July 14, 2012

Caching Bitmaps with LruCache

In the last exercise of "Implement Android Gallery widget with scale-down bitmap", the bitmap will be loaded and scaled every-time getView() of GalleryBaseAdapter called. The side-effect is the gallery widget stop a moment whenever a new a new item come-out.

The LruCache class (also available in the Support Library for use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently referenced objects in a strong referenced LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size. ~ reference: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html#memory-cache.

In this exercise, two Gallery widget were implemented. In the video shown below, the upper one is original gallery, the lower one is gallery with cached bitmaps. It can be noted that the cached gallery need more time to be loaded in first loading, but swipe much more smooth.



Main activity.
package com.example.androidgallery;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.LruCache;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.LinearLayout;

public class MainActivity extends Activity {

    public class GalleryBaseAdapter extends BaseAdapter {

     ArrayList<String> GalleryFileList;
     Context context;
     
     GalleryBaseAdapter(Context cont){
      context = cont;
      GalleryFileList = new ArrayList<String>();    
  }
     
  @Override
  public int getCount() {
   return GalleryFileList.size();
  }

  @Override
  public Object getItem(int position) {
   return GalleryFileList.get(position);
  }

  @Override
  public long getItemId(int position) {
   return position;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
   
   Bitmap bm = decodeSampledBitmapFromUri(GalleryFileList.get(position), 200, 200);
   LinearLayout layout = new LinearLayout(context);
   layout.setLayoutParams(new Gallery.LayoutParams(250, 250));
   layout.setGravity(Gravity.CENTER);
   
   ImageView imageView = new ImageView(context);
   imageView.setLayoutParams(new Gallery.LayoutParams(200, 200));
   imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
   imageView.setImageBitmap(bm);
   
   layout.addView(imageView);
   return layout; 
  }
  
  public void add(String newitem){
   GalleryFileList.add(newitem);
  }

 }
    
    public class CacheGalleryBaseAdapter extends GalleryBaseAdapter{

  CacheGalleryBaseAdapter(Context cont) {
   super(cont);
  }
  
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

   ImageView imageView = new ImageView(context);
   
   //---
   // Use the path as the key to LruCache
   final String imageKey = GalleryFileList.get(position);

      final Bitmap bm = getBitmapFromMemCache(imageKey);
      if (bm == null){
       BitmapWorkerTask task = new BitmapWorkerTask(imageView);
       task.execute(imageKey);
      }
   
   LinearLayout layout = new LinearLayout(context);
   layout.setLayoutParams(new Gallery.LayoutParams(250, 250));
   layout.setGravity(Gravity.CENTER);

   imageView.setLayoutParams(new Gallery.LayoutParams(200, 200));
   imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
   imageView.setImageBitmap(bm);
   
   layout.addView(imageView);
   return layout;

  }
     
    }
    
    GalleryBaseAdapter myGalleryBaseAdapter;
    CacheGalleryBaseAdapter myCacheGalleryBaseAdapter;
    Gallery myPhotoGallery, myFastGallery;
    
    private LruCache<String, Bitmap> mMemoryCache;
    
 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myPhotoGallery = (Gallery)findViewById(R.id.photogallery);
        myFastGallery = (Gallery)findViewById(R.id.fastgallery);
        
        myGalleryBaseAdapter = new GalleryBaseAdapter(this);
        myCacheGalleryBaseAdapter = new CacheGalleryBaseAdapter(this);
        
        String ExternalStorageDirectoryPath = Environment
    .getExternalStorageDirectory()
    .getAbsolutePath();
        
        String targetPath = ExternalStorageDirectoryPath + "/test/";
        
        File targetDirector = new File(targetPath);
        
        File[] files = targetDirector.listFiles();
        for (File file : files){
         myGalleryBaseAdapter.add(file.getPath());
         myCacheGalleryBaseAdapter.add(file.getPath());
  }
        
        myPhotoGallery.setAdapter(myGalleryBaseAdapter);
        myFastGallery.setAdapter(myCacheGalleryBaseAdapter);
        
        // Get memory class of this device, exceeding this amount will throw an
        // OutOfMemory exception.
        final int memClass = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

        // Use 1/8th of the available memory for this memory cache.
        final int cacheSize = 1024 * 1024 * memClass / 8;

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

         protected int sizeOf(String key, Bitmap bitmap) {
          // The cache size will be measured in bytes rather than number of items.
          return bitmap.getByteCount(); 
         }
         
        };
    }
 
 class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap>{
  
  private final WeakReference<ImageView> imageViewReference;
  
  public BitmapWorkerTask(ImageView imageView) {
         // Use a WeakReference to ensure the ImageView can be garbage collected
         imageViewReference = new WeakReference<ImageView>(imageView);
     }

  @Override
  protected Bitmap doInBackground(String... params) {
   final Bitmap bitmap = decodeSampledBitmapFromUri(params[0], 200, 200);
         addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
         return bitmap;
  }
  
  @Override
     protected void onPostExecute(Bitmap bitmap) {
         if (imageViewReference != null && bitmap != null) {
             final ImageView imageView = (ImageView)imageViewReference.get();
             if (imageView != null) {
                 imageView.setImageBitmap(bitmap);
             }
         }
     }
  
 }
 
 public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
  if (getBitmapFromMemCache(key) == null) {
   mMemoryCache.put(key, bitmap); 
  } 
 }

 public Bitmap getBitmapFromMemCache(String key) {
  return (Bitmap) mMemoryCache.get(key); 
 }
    
    public Bitmap decodeSampledBitmapFromUri(String path, int reqWidth, int reqHeight) {
     Bitmap bm = null;
     
     // First decode with inJustDecodeBounds=true to check dimensions
     final BitmapFactory.Options options = new BitmapFactory.Options();
     options.inJustDecodeBounds = true;
     BitmapFactory.decodeFile(path, options);
      
     // Calculate inSampleSize
     options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
      
     // Decode bitmap with inSampleSize set
     options.inJustDecodeBounds = false;
     bm = BitmapFactory.decodeFile(path, options); 
     
     return bm; 
    }
    
    public 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) {
      if (width > height) {
       inSampleSize = Math.round((float)height / (float)reqHeight);  
      } else {
       inSampleSize = Math.round((float)width / (float)reqWidth);  
      }  
     }
     
     return inSampleSize;  
    }

}


Modify the layout to have two gallery.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:orientation="vertical">
    
    <TextView 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:text="Gallery without Cache" />
    <Gallery
        android:id="@+id/photogallery"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
    
    <TextView 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:text="Gallery with Cache" />
    <Gallery
        android:id="@+id/fastgallery"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</LinearLayout>


android.util.LruCache is needed in the code, modify AndroidManifest.xml to have minSdkVersion and targetSdkVersion of "13".

Download the files.


Please note that this approach have it's own trade-off: if you request cacheSize too much, it will make your app causing java.lang.OutOfMemory exceptions, if too small, it will cause additional overhead. In my own openion, if you have predictable images (or resources) needed for caching, it's a good choice.

Read more: LruCache with different size

Related:
- Apply LruCache on GridView


6 comments:

  1. I stopped after reading that you create everytime new controls. This cannot be performant.

    ReplyDelete
  2. Thank You Sir .. Its very good tutorial & very useful for me.
    blessing for you :)

    ReplyDelete
  3. i downloaded ur project and run in eclipse..but showing NPE caused by for loop ..please help me...

    ReplyDelete
  4. i downloaded ur project and run in eclipse..facing NPE "Unable to start activity Component Info"..caused by for-each loop..tried a lot but nothing happened ..please help me..thnx in adv...

    ReplyDelete
  5. A really big thanks sir, i have been looking for examples on how to use lrucache and subsampling, i saw some examples but they're complex until i found your blog and example, i am able to understand it thank you very much! hope you'll keep on posting new stuffs

    ReplyDelete
  6. getting NPE, pls help

    ReplyDelete