My application shows an AlertDialog
with a ListView
inside. Everything worked fine bun then I decided to test this for memory leaks. After running
(2/12/2012): see UPDATE below.
This problem is not actually caused by the AlertDialog
but more related to the ListView
. You can reproduce the same problem by using the following activity:
public class LeakedListActivity extends ListActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Use an existing ListAdapter that will map an array
// of strings to TextViews
setListAdapter(new ArrayAdapter(this,
android.R.layout.simple_list_item_1, mStrings));
getListView().setOnItemClickListener(new OnItemClickListener() {
private final byte[] junk = new byte[10*1024*1024];
@Override
public void onItemClick(AdapterView> arg0, View arg1, int arg2,
long arg3) {
}
});
}
private String[] mStrings = new String[] {"1", "2"};
}
Rotate the device several times, and you'll get OOM.
I haven't got the time to investigate more on what is the real cause (I know what's happening but not clear why it's happening; can be bug, or designed). But here's one workaround that you can do, at least to avoid the OOM in your case.
First, you'll need to keep a reference to your leaked AlertDialog
. You can do this in the onCreateDialog()
. When you're using setItems()
, the AlertDialog
will internally create a ListView
. And when you set the onClickListener()
in your setItems()
call, internally it will be assigned to the ListView
onItemClickListener()
.
Then, in the leaked activity's onDestroy()
, set the AlertDialog
's ListView
's onItemClickListener()
to null
, which will release the reference to the listener an make whatever memory allocated within that listener to be eligible for GC. This way you won't get OOM. It's just a workaround and the real solution should actually be incorporated in the ListView
.
Here's a sample code for your onDestroy()
:
@Override
protected void onDestroy() {
super.onDestroy();
if(leakedDialog != null) {
ListView lv = leakedDialog.getListView();
if(lv != null) lv.setOnItemClickListener(null);
}
}
UPDATE (2/12/2012): After further investigation, this problem is actually not particularly related to ListView
nor to OnItemClickListener
, but to the fact that GC doesn't happen immediately and need time to decide which objects are eligible and ready for GC. Try this:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// this will create reference from button to
// the listener which in turn will create the "junk"
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
private byte[] junk = new byte[10*1024*1024];
@Override
public void onClick(View v) {
// do nothing
}
});
}
}
Rotate a couple of times, and you'll get the OOM. The problem is after you rotate, the junk
is still retained because GC hasn't and can't happen yet (if you use MAT, you'll see that this junk
is still retained by the button's listener deep down from the GCroot, and it will take time for the GC to decide whether this junk
is eligible and can be GCed.) But at the same time, a new junk
needs to be created after the rotation, and because of the mem alloc size (10M per junk), this will cause OOM.
The solution is to break any references to the listener (the junk
owner), in this case from the button, which practically makes the listener as a GCroot with only short path to the junk and make the GC decide faster to reclaim the junk memory. This can be done in the onDestroy()
:
@Override
protected void onDestroy() {
// this will break the reference from the button
// to the listener (the "junk" owner)
findViewById(R.id.button).setOnClickListener(null);
super.onDestroy();
}