OutOfMemoryError, ImageView and TransitionDrawable - A Sneaky Memory Leak with Improper use of TransitionDrawables with ImageViews
We like to use animations and gradual transitions instead of abrupt view changes, so a lot of our ImageViews use TransitionDrawable instead of directly displaying an image.
Definition of TransitionDrawables from the Android documentation:
An extension of LayerDrawables that is intended to cross-fade between the first and second layer.
Today we ran into a scenario that was resulting in memory leaks and ultimately an OutOfMemoryError after a number of transitions. We tracked down the issue, and I wanted to share it here in case anyone else is running into similar problems.
The scenario is this - we’re displaying a series of images that we download over the internet. When we create the activity we display a placeholder image and then, when the first image is finished downloading, we display a TransitionDrawable consisting of 1) the previous image and 2) the newly downloaded image. Then, as we download each successive image, we display a new TransitionDrawable containing 1) whatever the ImageView was displaying before and 2) the newly downloaded image.
The code looks like this:
arrayDrawable[0] = imageView.getDrawable();
arrayDrawable[1] = newBitmapDrawable;
TransitionDrawable transitionDrawable
= new TransitionDrawable(arrayDrawable);
transitionDrawable.setCrossFadeEnabled(true);
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(500);
(do you see the problem?)
The code resulted in an ever-growing native memory heap, a good indication of a memory leak involving bitmaps. (the memory used for Bitmaps is allocated using native code, and is not garbage collected, but is released by the bitmap’s destructor instead)
First thing we tried was to call recycle() manually. This resulted in the android.graphics.Canvas.throwIfRecycled RuntimeException, indicating that the bitmap was still being used, and something is trying to draw it on the screen. This was a hint - whatever was holding onto it and preventing us from recycling() it, was also responsible for the memory leak evident when observing the native heap.
After a bit of digging around, the problem became obvious: Because we always use the old Drawable from the ImageView as the start state for the new TransitionDrawable, and then we set it right back as the new ImageView drawable, each bitmap was being retained in an infinitely growing stack of nested TransitionDrawables.
Let me illustrate this with a bit of pseudocode:
First transition, from placeholder Bitmap to newly downloaded Bitmap:
TransitionDrawable
{
layers:[
0:bitmap,
1:bitmap
]
}
Second transition:
TransitionDrawable
{
layers:[
0:TransitionDrawable
{
layers:[
0:bitmap,
1:bitmap
]
},
1:bitmap
]
}
Third transition:
TransitionDrawable
{
layers:[
0:TransitionDrawable
{
layers:[
0:TransitionDrawable
{
layers:[
0:bitmap,
1:bitmap
]
},
1:bitmap
]
}
1:bitmap
]
}
And so on…
Once we pinpointed the problem, the fix was easy. There are two options:
1) after each transition, reset the imageView drawable to the new BitmapDrawable - the layer 1, or the final state of the TransitionDrawable. Since the TransitionDrawable does not have a completion listener, you wil need to schedule the resetting yourself.
//use your scheduling mechanism of choice
//Ticker, Handler.postDelayed
imageView.setImageDrawable(drawable);
This has the benefit of making sure that only 1 bitmap is retained post transition.
2) On each successive transition, check to see if the ImageView already contains a TransitionDrawable, and then grab the currently visible layer (layer 1 / final layer) to use as the starting state for the new transition.
Drawable oldDrawable = imageView.getDrawable();
BitmapDrawable oldBitmapDrawable = null;
if (oldDrawable instanceof TransitionDrawable)
{
TransitionDrawable oldTransitionDrawable = (TransitionDrawable)oldDrawable;
oldBitmapDrawable =(BitmapDrawable) (oldTransitionDrawable).findDrawableByLayerId(1);
}
else if (oldDrawable instanceof BitmapDrawable)
{
oldBitmapDrawable = (BitmapDrawable) oldDrawable;
}
Either of these approaches will prevent the infinitely growing stack of TransitionDrawables, and allow the Bitmaps to be recycled when no longer needed.