Android Memory Management #4: Handling Bitmaps

Android Memory Management #4: Handling Bitmaps

Loading bitmaps in an Android app is a tricky task. Bitmap takes an enormous amount of memory. For example, a photo taken on Pixel 6 Pro has about 12.5 megapixels (4080 x 3072). With the bitmap configuration of ARGB_8888 (4 bytes per pixel), loading that photo into memory takes about 50 MB of memory. Such large bitmaps can quickly exhaust an app's memory budget.

Fortunately, there are a lot of libraries that help us with this matter. Some of the popular image-loading libraries include Picasso from Square, Coil from Instacart, Fresco from Facebook and Glide from Typeguard, Inc. These libraries simplify most of the complex tasks associated with bitmaps and other types of images on Android, such as fetching, decoding and displaying bitmaps. It is nice to understand how these libraries can deal with bitmap efficiently and make our life easier. In this article, we will talk about some of the problems we might face when handling bitmap and how to solve them.

bmp.png

The problems with Bitmap

We have already discussed that Bitmaps can take a massive amount of application memory. That will likely lead to several problems:

  • Out of memory error can occur
  • Slow responsiveness on UI or even ANR
  • Slow fetching and displaying of an image

Let’s see how to solve these problems.

Out of memory

Images come in all shapes and sizes. In most cases, they are larger than the UI components required to display them. There is no point to load a 4080 x 3072 image into memory and then only showing it on a smaller 1020 x 768 view. An image with a higher resolution does not provide any visible benefit, but still takes up precious memory. Ideally, we only want to load a smaller version of the image into the memory. How can we do that?

First, we need to find out how big the image is. The BitmapFactory class provides an option called inJustDecodeBounds, which can be used to read the dimensions and type of the image without actually allocating memory for that bitmap.

bitmap_injustdecodebounds_quote.png

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

After finding out the image dimensions, we need to load a scaled-down version of it to match the dimensions of the target UI component that the image is to be loaded into. To downsampling an image, we can use the inSampleSize option. For example, an image with resolution 4080 x 3072 (~50 MB) that is decoded with an inSampleSize of 4 produces a bitmap of approximately 1020 x 768 (~3 MB). Notes that the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height > reqHeight || width > reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

You can read the full code here at the official guide.

Slow responsiveness on UI

Loading bitmaps on the UI thread can degrade your app's performance. Therefore, it is important to decode bitmaps on a background thread. Moreover, we should build a cache on memory to fast-access the decoded bitmaps.

In Android, the recommended way to make a bitmap cache is to use LruCache. In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache. However, in the latter Android version, the garbage collector is more aggressive in collecting soft/weak references, which makes them fairly ineffective. To make a good LruCache, we should take into consideration some factors, such as:

  • How memory intensive is the rest of your activity and application, and how much memory each bitmap in the cache will take?
  • The trade-off between quality and quantity. Is it better to store a larger number of lower-quality bitmaps?
  • How many images will be on-screen at once and how frequently will the images be accessed?

Answering these questions will help us determine the suitable size for a cache.

Besides that, bitmaps are large, so it makes the Garbage Collector (GC) run more frequently. Here you can use the Bitmap Pool to make it more efficient to handle bitmap. Bitmap Pool will reuse bitmap, avoids continuous allocation and de-allocation of memory in your application, reduces GC overhead. The less time GC run, the more time for our app to do the job.

Slow downloading image

Sending data across the network can take a long time, especially with large data like bitmap. A disk cache can often help here, allowing components to quickly reload downloaded images. Of course, fetching images from disk is slower than loading from memory, but it is still very useful because the memory cache is not always available. Memory cache can be filled up quickly with a large dataset, or get destroyed if the application is in the background for too long.

In an application where the images are accessed more frequently, like an image gallery application, a ContentProvider might be a more appropriate place to store cached images.

Conclusions

It is fundamental to know how libraries work under the hood, but you don’t need to reinvent the wheel. Image-loading libraries such as Glide or Coil abstract out most of the complexity in handling these and other tasks related to working with bitmaps and other images on Android for us. You can read more about them here: Glide, Coil.

Thanks for reading.

References

Loading Large Bitmaps Efficiently

Caching Bitmaps

How The Android Image Loading Library Glide and Fresco Works?