Saturday, June 7, 2014

Avoiding Out of Memory errors while loading large bitmaps

Hi everyone! Has been a long time since I wrote here, and it is time to clean the dust and publish new and hopefully useful stuff :)
Today I will present some suggestion on how to minimize the amount of RAM while loading large bitmap on Android, in order to avoid the common OutOfMemory error.

New mobile phones and tablets have constantly seen their display to become more capable in term of display resolution. This brings many positive aspects, but for developers, usually a lot of headaches: more capable displays means much more memory required to display images and pictures on them.

An image (a PNG, a JPEG..) is always displayed as a matrix of pixels. Supposing a 32 bit depth color, each pixel is represented as 32 bits, i.e. 4 bytes (R,G,B and alpha component).
On a modern display (for example, let's take the Nexus 5), we have 1080x1920 pixels. Potentially, an app that shows an image fullscreen (for simplicity we pretend the black bar with status bar and Android system buttons is hidden) must load in memory a bitmap: 4 * 1080 * 1920 = 8294400 bytes big.

This would not be a problem if the only thing loaded in the current instance of dalvik VM is that image, but of course in memory we can have much more objects.

How much memory does Android borrow to a single app? It depends. Each Android device might in theory have a different threshold which dictates how much memory is granted to the application. It is declared as a system property and can be read by typing this on the console:

adb pull /system/build.prop
cat build.prop

and checking the property dalvik.vm.heapgrowthlimit 
In general, devices with bigger dpi screens have a higher value of heap limit. For example, the LG Nexus 5, a XXHDPI device, has 192MB of limit. As said, device with the same resolution might have different heap limit: an HTC One X (XHDPI device) has 92MB of limit; a Samsung S3 (also XHDPI) has only 64MB of heap limit.

An app can request more RAM, by declaring the android:largeHeap property in the Android manifest. How much RAM can be granted? Also this property is defined in the same file (dalvik.vm.heapsize), and in case of my Nexus 5, is 512 MB.
Why not just using always this property? Because the GC tends to become inefficient and app will be much slower. 

Consequently, in mostly all cases, instead of setting this attribute we need to minimize the amount of RAM needed, especially if we want to tackle the well known OutOfMemory exception.

In order to minimize the possibility of such error, a well-behaving app that need to load a large bitmap must:
- check the amount of free memory left 
- adopt strategies in case the available memory is not enough to load it.

Check the amount of free memory left

Android inherits the Runtime clas sfrom Java standard library. This helps getting the information we fetched via console: in particular, how much memory is available, how much free memory is left before the VM will complain.

In the gist above, I calculate the total available heap (192MB on my device) and the available heap (it was around 175MB before loading the bitmap).

Check how big is the bitmap we want to load

On Android3.0+, we can use a handy class called BitmapFactory.Options. It does a lot of useful things, one of which is giving info about the image, without loading it in memory.

In the snippet above, I use the aforementioned class to calculate image width and height without loading it in RAM, by setting to true the inJustDecodeBounds property. We can have a rough aestimate of how large will be the image in RAM from its width, height attributes.

Load a subscaled version of the bitmap in RAM

If the memory available is less than the amount of memory required by the bitmap, then we apply some subsampling. The image will be imperceptibly lower in quality on the specific device where this subsampling will happen, but we will avoid the Out of Memory error. User will be happy and probably won't even notice the difference.
Again, to subsample the image we use the BitmapFactory.Options:

We use power of 2 because the decoder will anyway round other values to a power of 2. Also, I placed a limit o 8 but also this can be avoided and image can be subsampled at any level.

By subsampling an image with a factor of 2 you can see you will be able to spare almost 75% of memory, and the difference in quality is almost negligible.

You can download the sample app which contains all these snippets and a demo activity you can play with here: