Android: AlertDialog causes a memory leak

前端 未结 4 604
天命终不由人
天命终不由人 2020-12-05 05:47

My application shows an AlertDialog with a ListView inside. Everything worked fine bun then I decided to test this for memory leaks. After running

4条回答
  •  失恋的感觉
    2020-12-05 06:17

    (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 junkowner), 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();
    }
    

提交回复
热议问题