The Pain of Displaying Images on Android
When working on developing visually engaging apps, displaying images is a must. The problem is that Android OS doesn’t handle image decoding very well, forcing the developer to take care of certain tasks to avoid messing up performance.
At least, Google wrote a complete guide about Displaying Bitmaps Efficiently, which we can follow to understand and solve the main flaws of Android OS when displaying Bitmaps.
Android App Performance Killers
Following Google’s guide, we can list some of the main problems we come across when displaying images on our Android app.
Image downsample
Android decodes and displays images at its full dimensions / size, regardless of the viewport size. Due to this, if you try to load a heavy image, you can easily cause an outOfMemoryError
on your device.
To avoid this, as stated by Google, we should use the BitmapFactory
to decode the image, setting a value for inSampleSize
parameter. Image dimensions are divided by inSampleSize
, reducing the amount of memory used.
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
You can manually set inSampleSize
, or calculate it using display dimensions.
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;
}
Asynchronous Decoding
Even when using BitmapFactory
, image decoding is done on UI thread. This can freeze the app and cause ANR (“Application Not Responding”) alerts.
This one is easy to solve, you just have to place the decoding process on a worker thread. One way to do this is using an AsyncTask, as explained on Google guide:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
Image Caching
Every time an image is decoded and placed in a view, Android OS repeats the entire rendering process by default, wasting precious device memory. If you are planning to show the same image in different places, or reload it multiple times due to your app life cycle or behaviour, this can be especially annoying.
In order to avoid consuming too much memory, using a memory and a disk cache is recommended. Next, we are going to see the main differences between those caches, and why it’s useful to use both at the same time. Code is too complex to show it here, so please refer to the Caching Bitmaps section of the Google Guide to learn how you can implement your own memory and disk cache.
- Memory Cache: Images are stored in the device memory. Memory is fast to access. Much faster, indeed, than the image decoding process, so storing images here is a good idea to make your app faster and more stable. The only cons of Memory Cache is that it is only alive during the app life cycle, which means that once the app is closed or killed (entirely or partially) by Android OS memory manager, all images stored there will be lost. Keep in mind that memory cache must set a max usable memory amount. Otherwise, it could cause the famous
outOfMemoryError
. - Disk Cache: Images are stored in the device physical storage (disk). Disk cache is kept alive between app launches, storing images safely as long as it has enough space to store them. The downside is that disk read and write operations can be slow, and are always slower than accessing memory cache. Due to this, all disk operations must be performed on a worker thread, outside the UI thread. Otherwise, your app can freeze and cause ANR alerts.
Every cache has its pros and cons, so the best practice is using both and reading images from where they are available first, starting by memory cache.
Final Thoughts and EpicBitmapRenderer
As you may have already noticed, and as I stated at the beginning of this article, showing images on your Android app can be a real headache. It is certainly not the easy task it seemed, as it is in other environments.
In order to avoid repeating these tasks in every project, I developed a 100% free, open source Android library called EpicBitmapRenderer
. You can fork it on EpicBitmapRenderer GitHub repo, and learn more about it at EpicBitmapRenderer website.
EpicBitmapRenderer
is easy to use, and automatizes all these annoying tasks in every image decoding operation, so you can focus on your app development.
You just have to add the EpicBitmapRenderer
dependency on your gradle (to see alternatives for other build tools, take a look at the Importing Library section of EpicBitmapRenderer documentation).
compile 'com.isaacrf.epicbitmaprenderer:epicbitmaprenderer:1.0'
Decoding an image in EpicBitmapRenderer
is really easy: just call the desired decoding method and manage the result. Take a look at this example, where we get an image from an URL and display it on an ImageVIew
.
EpicBitmapRenderer.decodeBitmapFromUrl(
"http://isaacrf.com/wp-content/themes/Workality-Lite-child/images/IsaacRF.png",
200, 200,
new OnBitmapRendered() {
@Override
public void onBitmapRendered(Bitmap bitmap) {
ImageView imgView = findViewById(R.id.imgSampleDecodeUrl);
imgView.setImageBitmap(bitmap);
}
},
new OnBitmapRenderFailed() {
@Override
public void onBitmapRenderFailed(Exception e) {
Toast.makeText(MainActivity.this,
"Failed to load Bitmap from URL",
Toast.LENGTH_SHORT).show();
}
});
The post Displaying Bitmaps Efficiently on Android apps appeared first on Isaac RF.