If you develop apps, you'll probably (at some point) need to load images from the internet. Sometimes you'll load many images at once, as in a ListView or Gallery. Your first foray will be easy, using BitmapFactory to decode a stream - then you run into the dreaded OutOfMemory error and are suddenly grounded back to the reality that is limited memory space.
There are two equally important problems to handle when using images: loading an image to view it, and unloading it later to save memory. Last time I looked there were plenty of libraries out there for solving the first problem, but I was unsatisfied with their solutions to the second. A lot of them simply blow away loaded Bitmaps at some point in time - but what if you still wanted to use it? Then you end up in this complex tightrope walk, where you're having to constantly check and re-check your Bitmaps for whether they still exist. It's a gigantic pain and I've been doing this dance forever.
I set out to solve the problem in a better way, such that it would do three things:
1. Load images from a URL into a View.
2. Unload that image later (for memory's sake).
3. Re-load that URL into the View if we ever see it again.
I was able to accomplish all three with the help of a class I wrote, UrlBitmapDrawable.
You can check out the code here: https://gist.github.com/dlew/69e6557604926d7e1513
You can use it just about anywhere as a Drawable. Just instantiate it then use it:
UrlBitmapDrawable drawable = new UrlBitmapDrawable(res, myUrl, myFallback);
With ImageViews there's a bit of a hack I needed to do (in order not to have to use a custom ImageView). So I setup another method for using UrlBitmapDrawable with ImageViews:
UrlBitmapDrawable.loadImageView(myImageUrl, myImageView, myFallback);
This solution has made my life easier in four ways:
- All you need to is provide it a URL and it takes care of the rest. You don't ever have to worry about its state; if the underlying Bitmap is recycled, it will fallback to the default image and start reloading the URL.
It's a Drawable
- By making it a Drawable, it meant I could attach it to any View. Tying it to a custom View would have vastly limited its potential.
- It's not tied to any particular implementation for loading images. Retrieving images could be a simple network call, or you could hook it up to a complex LRU memory/disk cache.
- The version I've provided is simple; internally we've added some bells and whistles to it. See "Areas for Improvement" below.
It's not all sunshine and daisies. There are two problems with UrlBitmapDrawable; however, I considered them minor in comparison to the larger problem I was trying to solve.
- BitmapDrawable does not let you access the underlying Bitmap by default, so you'll have to import your own version that opens it up. Here's the source code from github
- In order to get ImageView to re-measure the Drawable's width/height after loading you have to trick it into thinking the Drawable has changed (by nulling it then resetting it back). To be honest, there might be a better solution here, but I haven't found it.
Areas for Improvement
Here's ways that we've tricked out our UrlBitmapDrawable:
Default Resources via Id
- A default resource is important (so we can show the user something before we load the asset). In the sample code the UrlBitmapDrawable holds a Bitmap; this is a fine example, but if you're inflating a new default Bitmap per UrlBitmapDrawable, that can wreak its own memory havoc.
Internally, we've gone a more complex route which uses Resources and resIds, and loads the Bitmap automatically from an in-memory cache. It keeps us from spending a lot of time (and memory) reloading the same default bitmaps.
- We are sometimes given a list of URLs at differing quality levels. It's pretty easy to hook that into this system; each time a download fails, try the next URL.
- If you read the code carefully you may notice that I never actually dealt with the second step - unloading Bitmaps from memory. We use an LruCache to handle this; as a result of UrlBitmapDrawable it can recycle Bitmaps with impunity. It also means you can evict the entire cache at any time if you need the memory.