Reduce Your Android Memory Footprint with nodpi Assets

We reduced the memory size of our loading animations by 90%

I think it’s safe to say that we all hate out of memory (OOM) errors. You know the drill: you build an awesome new feature, release it to your users and then see these crop up all over your crash reports.

java.lang.OutOfMemoryError: Failed to allocate a 2793484 byte allocation with 10560 free bytes and 10KB until OOM

at dalvik.system.VMRuntime.newNonMovableArray(VMRuntime.java)

at android.graphics.Bitmap.nativeCreate(Bitmap.java)

This is exactly what happened to us at Redfin when we tried adding loading animations for sections on our Listing Details Pages. We need to make a network request for many of our sections, so we can’t display them immediately. Having placeholder images with loading animations is a nice way to show we’re waiting for information, and it limits how much the page is re-arranged once we finally get that information.

We were able to implement these animations using Facebook’s open source ShimmerFrameLayout. We created density qualified (hdpi, xhdpi, etc.) placeholder images and set these on an ImageView. We then wrapped these ImageViews in a ShimmerFrameLayout. This gave us a great loading animation for each of our sections:

Rendered Loading Mask

But when we released these animations, we started seeing a ton of OOM errors in our production logs. We found that our loading animations were taking up way more memory than we anticipated. We really had two related issues:

The Bitmap referenced by our ImageView was using a lot of memory The Bitmaps used by our ShimmerFrameLayout for our animation were using even more memory

Animations with ShimmerFrameLayout

ShimmerFrameLayout is a great resource for easily creating these types of animations. However, it is a general layout—it will take any child views you supply it with and render the shimmer animation over them. This makes it really easy to work with, but it isn’t really optimized for wrapping a single ImageView.

ShimmerFrameLayout works by creating three bitmaps that are the size of the FrameLayout itself. One contains a desaturated drawing of the FrameLayout’s child views, the other contains a saturated version and the third contains a mask drawn which is drawn on top of the saturated version. On each dispatchDraw operation it draws each of these bitmaps to the parent canvas, and then adds an offset to drawing the mask. The details of how this works can be found in the layout’s dispatchDraw method.

Calculating our Memory Footprint

To calculate our memory footprint, we’ll step through the different bitmaps being used for the shown animation, and we’ll take a Galaxy S6 as our test device.

The S6 is a xxxhdpi device, so our placeholder image had a resolution of 1211x753. This led to the bitmap object stored on the image view alone being over 3.5mb. The ShimmerFrameLayout then allocated its three bitmaps which are each the size of the layout itself. On the S6 this meant these bitmaps were 1314x753, coming in at 4mb a piece. So for a single loading animation we had 15.5mb of memory allocated — 12mb for the bitmaps on the ShimmerFrameLayout and another 3.5mb for the bitmap stored on the ImageView.

This isn’t the only section we show a loading animation for, and though the memory allocation varies by section it’s reasonable to say we were using upwards of 55mb of memory for a single details page!

My initial thought when trying to solve this issue was to just get rid of all resolutions of placeholder images greater than hdpi. These are just placeholder assets in a loading animation, so we didn’t need them to really be high resolution crisp images. However, this didn’t work at all. Android will take these low res images and scale them up for our ImageView. We’ll get a blurry image, since we’re starting off with a low res asset, but the bitmap will still take up 3.5mb. Also the bitmaps on the ShimmerFrameLayout will be unchanged, so we won’t have actually saved any memory at all.

Using nodpi Assets

The official Android documentation describes nodpi assets as:

Resources for all densities. These are density-independent resources. The system does not scale resources tagged with this qualifier, regardless of the current screen’s density.

This was perfect for what we needed. The system wouldn’t scale our images when creating bitmaps, so our memory footprint would actually go down using these images. We moved our hdpi image to the drawable-nodpi directory, and deleted all the density qualified placeholders. Density qualified drawables take precedent over those in the nodpi directory, so you have to delete those. We then modified our ImageView to specify its height in xml instead of just wrapping content. Our code is now changed to:

Since we specified the height, ImageView will now use matrix operations to scale the image being displayed. Our displayed image ended up being the correct size, while the bitmap referenced by ImageView is much smaller. The size of this bitmap went from 3.5mb down to 670kb.

With a small modification to ShimmerFrameLayout we were able to get its three bitmaps to respect these sizes as well. This dropped the size of each bitmap on the Shimmer from 4mb each down to 670kb each. Our overall memory footprint for this animation dropped from 15.5mb down to about 2.5mb! You can take a look at the modifications to ShimmerFrameLayout in this pull request.

Memory Footprint (mb)

Using nodpi images doesn’t work for every use case, but it can help a lot if you need to display large images and don’t care much about the image resolution. With them we were able to achieve a much more manageable memory footprint and pretty much eliminated all our OOM errors on our Details Pages.