Basic Bitmap Caching in Android

by Samuel Attard

A major problem I faced when messing around with android development was fetching images from a remote server and displaying them in an ImageView. There is a neat bit of code that does this for you but it had one major floor. Caching was non-existent, every time you requested the image it fetched it again from the server, this meant in my use case between 20 and 30 profile pictures had to be freshly downloaded each time. This made the UI slow to load and created a far from ideal user experience. Below are the steps I took to make my own Bitmap caching tool.

Fetching the Images

class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {  
    ImageView bmImage;
    Activity context;

    public DownloadImageTask(ImageView bmImage, Activity context) {
        this.bmImage = bmImage;
        this.context = context;
    }

    protected Bitmap doInBackground(String... params) {
        String urlStr = params[0];
        Bitmap img = null;

        HttpClient client = new DefaultHttpClient();
        HttpGet request = new HttpGet(urlStr);
        HttpResponse response;
        try {
            response = (HttpResponse) client.execute(request);
            HttpEntity entity = response.getEntity();
            BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
            InputStream inputStream = bufferedEntity.getContent();
            img = BitmapFactory.decodeStream(inputStream);
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return img;
    }

    protected void onPostExecute(Bitmap result) {
        bmImage.setImageBitmap(result);
    }
}

To use this neat class you simply call

new DownloadImageTask(YourImageView, this.getApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, "http://your.url/here");  

The executeOnExecutor component is used so that images can be fetched Asynchronously at the same time (You can load more than one image at once) there will be no queue.

Caching these images

Now we can fetch images, we want to cache them, to do this we use a simple SQLite database. The code for the BitmapCache is below

public class BitmapCache extends SQLiteOpenHelper {  
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "BitmapCache.db";
    private static final String SQL_CREATE_ENTRIES = "CREATE TABLE bitmaps (key TEXT, bitmap BLOB)";
    private static final String SQL_DELETE_ENTRIES = "DELETE FROM bitmaps WHERE 1";

    public BitmapCache(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        Log.d("BitmapCache", "Bitmap cache created");
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }

    public void addBitmapToCache(String key, Bitmap bitmap) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("key", key);
        values.put("bitmap", DbBitmapUtility.getBytes(bitmap));
        db.insert("bitmaps", null, values);
    }

    public Boolean isBitmapInCache(String key) {
        Cursor c = getKeyCursor(key);

        return (c.moveToFirst() && c.getCount() != 0);
    }

    public Bitmap getBitmapFromCache(String key) {
        Cursor c = getKeyCursor(key);

        c.moveToFirst();
        byte[] tmp = c.getBlob(c.getColumnIndex("bitmap"));
        Bitmap img = DbBitmapUtility.getImage(tmp);
        c.close();
        return img;
    }

    private Cursor getKeyCursor(String key) {
        SQLiteDatabase db = this.getWritableDatabase();
        String[] projection = {"key", "bitmap"};
        String sortOrder = "key DESC";

        return db.query(
                "bitmaps",  // The table to query
                projection,                               // The columns to return
                "key = ?",                                // The columns for the WHERE clause
                new String[]{key},                            // The values for the WHERE clause
                null,                                     // don't group the rows
                null,                                     // don't filter by row groups
                sortOrder                                 // The sort order
        );
    }
}

class DbBitmapUtility {  
    // convert from bitmap to byte array
    public static byte[] getBytes(Bitmap bitmap) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream);
        return stream.toByteArray();
    }

    // convert from byte array to bitmap
    public static Bitmap getImage(byte[] image) {
        return BitmapFactory.decodeByteArray(image, 0, image.length);
    }
}

The BitmapCache has two components, the SQLiteOpenHelper which handles transactions with the database, and the DbBitmapUtility which converts Bitmaps between Blobs (Byte Arrays) and Bitmaps. Integrating this BitmapCache with the DownloadImageTask is as simple as adding an if statement around the downloader like so.

class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {  
    ImageView bmImage;
    Activity context;

    public DownloadImageTask(ImageView bmImage, Activity context) {
        this.bmImage = bmImage;
        this.context = context;
    }

    protected Bitmap doInBackground(String... params) {
        BitmapCache cache = new BitmapCache(this.context.getApplication());
        String urlStr = params[0];
        Bitmap img = null;

        if (!cache.isBitmapInCache(urlStr)) {
            HttpClient client = new DefaultHttpClient();
            HttpGet request = new HttpGet(urlStr);
            HttpResponse response;
            try {
                response = (HttpResponse) client.execute(request);
                HttpEntity entity = response.getEntity();
                BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
                InputStream inputStream = bufferedEntity.getContent();
                img = BitmapFactory.decodeStream(inputStream);
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            cache.addBitmapToCache(urlStr, img);
        } else {
            img = cache.getBitmapFromCache(urlStr);
        }
        return img;
    }

    protected void onPostExecute(Bitmap result) {
        bmImage.setImageBitmap(result);
    }
}

Now the download task will only download the image again if it is not present in the cache. This greatly decreases render time and makes the UI significantly smoother as it means you only have to load the images once.

In Conclusion

If you can you should always include images as static resources inside the application, but if dynamic web based images are required (such as profile photos) using this caching technique is very useful. If you want to force a new download of images, just add a ?new={completelyrandomnumberhere} to the end of your URL, then it won't fetch the cached version.

Hopefully this helps some people out, good luck out there :)