In the previouspost, we have briefly introduced memory leak in Android. Memory leak is one of the most important issues, we need to pay attention to during Android development. It may happen when we are unaware of it. In this post, we would like to share a scenario of memory leak in Gingerbread, which is diffcult to notice and observe.
Scenario
Firstly, let’s take a look at a simple example:
1234567891011
publicclassSecondActivityextendsActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_second);Drawabledrawable=DrawableLruCache.getInstance().getDrawable(R.drawable.android_logo);ImageViewimageView=(ImageView)findViewById(R.id.image_view);imageView.setImageDrawable(drawable);}}
This piece of code is very simple. In an Activity, we would like to get a Drawable from a LRU cache, and then show the Drawable in a ImageView inside the Activity. Could you figure out anything wrong with this piece of code?
Actually, this piece of code would cause memory leak in Gingerbread devices. When the Activity is destroyed, the LRU cache would likely continue to keep a strong reference of the Drawable, and the Drawable would still keep a strong reference of the View. Therefore, the whole Activity, as well as everything inside the Activity would be memory leaked.
Reason
The memory leak is caused by a small “bug” in the design of the Drawable object in Gingerbread.
In Android, all the Views implement theDrawable.Callbackinterface. This interface is required for the Views to support animated Drawable. Whenever we need to display the Drawable in the View, for instance when we callingsetImageDrawable(Drawable drawable)in a ImageView, Android would set the view as the callback client of the Drawable, in order to allow the View to support animation for the Drawable. You may figure out the detail implementation of the method from the source code of Androidhere.
However, according to thesource codeof the Drawable, the Callback of the Drawable is passed as a strong reference in Gingerbread shown below:
123
publicfinalvoidsetCallback(Callbackcb){mCallback=cb;}
That means, whenever we want to display the Drawable to a View, the Drawable would keep a strong reference of the View. Therefore, even after the Activity or View is destroyed, the Drawable would still keep a strong reference to the View, which may cause memory leak.
You may think it is the LRU cache causes the memory leak. However, even if we do not use the LRU cache, this memory leak may still happen easily. The reason is that Android does have some kind of recycling manager with Drawable component. Therefore, when we get the Drawable with the same resource ID at different places, it is very common that Android would return the same instance of Drawable in order to optimize the usage of the memory.
In other words, even there is no LRU cache, the same Drawable may still be used by other components such as another ImageView in another Activity, as long as the other ImageView needs to show the same Drawable with the same resource ID. If the other ImageView exists in the memory, it would keep a strong reference of the Drawable. Consequently, the Drawable would keep a strong reference of the View and will cause the same memory leak.
Solution
In order to solve this kind of memory leak, what we need to do is to manually set the Callback to benullwhenonDestory()is called in the Activity, or whenonDetachedFromWindow()is called for the specific View. The source code we used to unbind the Drawable from the root view of the Activity is shown below:
1234567891011121314151617181920212223242526272829
publicstaticvoidunbindDrawables(Viewview){if(view==null){return;}if(view.getBackground()!=null){view.getBackground().setCallback(null);}if(viewinstanceofImageView){ImageViewimgView=(ImageView)view;if(imgView.getDrawable()!=null){Drawableb=imgView.getDrawable();b.setCallback(null);imgView.setImageDrawable(null);}}elseif(viewinstanceofViewGroup){for(inti=0;i<((ViewGroup)view).getChildCount();i++){unbindDrawables(((ViewGroup)view).getChildAt(i));}try{if((viewinstanceofAdapterView)){AdapterViewadapterView=(AdapterView)view;adapterView.setAdapter(null);}else{((ViewGroup)view).removeAllViews();}}catch(Exceptione){}}}
One more thing need to mention is that, this issue has already fixed in the Android Ice Cream Sandwich and above. In the newer version of Android, the Callback would be passed as aWeakReferenceaccording to the source codehere. Hence, if you are no longer supporting Gingerbread, you don’t need to care about this issue