16

My application shows an AlertDialog with a ListView inside. Everything worked fine bun then I decided to test this for memory leaks. After running the app for some time I opened MAT and generated Leak Suspects report. MAT found several similar leaks:

One instance of "com.android.internal.app.AlertController$RecycleListView" loaded by "<system class loader>" occupies ...

I spent a lot of time searching for the reason of this leak. Code review didn't help me and I started googling. That's what I found:

Issue 5054: AlertDialog seems to cause a memory leak through a Message in the MessageQueue

I decided to check whether this bug reproduces or not. For this purpose I created a little program which consists of two activities. MainActivity is an enrty point. It contains only a buttons which runs LeakedActivity. The latter just shows an AlertDialog in its onCreate() method. Here's the code:

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        findViewById(R.id.button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(
                    new Intent(MainActivity.this, LeakedActivity.class));
            }
        });
    }
}

public class LeakedActivity extends Activity {
    private static final int DIALOG_LEAK = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            showDialog(DIALOG_LEAK);
        }
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        if (id == DIALOG_LEAK) {
            return new AlertDialog.Builder(this)
                .setTitle("Title")
                .setItems(new CharSequence[] { "1", "2" },
                    new OnClickListener() {
                        private final byte[] junk = new byte[10*1024*1024];

                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            // nothing
                        }
                    })
                .create();
        }
        return super.onCreateDialog(id);
    }
}

MAT reports this application leaks com.android.internal.app.AlertController$RecycleListView every time the AlertDialog is dismissed and the LeakedActivity is finished.

I can't find any error in this small program. It looks like a very simple case of using AlertDialog and it must work well but seems it doesn't. So I'd like to know how to avoid memory leaks when using AlertDialogs with items. And why hasn't this problem been fixed yet? Thanks in advance.

Michael
  • 53,859
  • 22
  • 133
  • 139

4 Answers4

29

(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<String>(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();
}
Ricky Lee
  • 1,039
  • 6
  • 6
  • Great. Would you mind marking your question as answered so that other people with similar problem can easily find the solution. – Ricky Lee Aug 21 '11 at 20:48
  • Certainly, but a little later. If nobody gives a better answer in four days then I'll accept your answer and award it a bounty. – Michael Aug 22 '11 at 04:05
  • 2
    Yes of course. My point is that there are lots of questions with good answers in stackoverflow but are not marked answered (the answer was not accepted). It'll be good for the community if everyone who asks be a bit responsible and mark/accept the answer so that it will be more helpful for others with similar problem. – Ricky Lee Aug 22 '11 at 08:14
  • 1
    Your answer is the best so the bounty is yours. – Michael Aug 25 '11 at 21:12
  • 1
    Hi , in my situation , it is not about SetOnClickListiner , but about Views and Image resources used in each ListViewItem , How can I solve this problem ? – Ata Mar 25 '12 at 04:32
  • @Ata i am facing your problem did you find any solution – user4o01 Feb 09 '14 at 19:16
  • @user4o01 , I do not remember , as it was too old, but I think you have to null out images , imgView.setImageBitmap(null); this cause remove loaded image pointer and when GC called, it frees the image resource. – Ata Feb 10 '14 at 07:27
1

Cannot reproduce for Android 2.3.4 2.3.3. I Tested your exact code on a actual device and from what I see in LogCat, the heap size stays constant over time. Sadly I was not able to hprof-conf my dumps (ERROR: expecting 1.0.3)

08-19 08:41:58.026: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 13K, 50% free 2698K/5379K, external 83K/519K, paused 16ms
08-19 08:41:58.056: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1K, 43% free 3720K/6471K, external 83K/519K, paused 18ms
08-19 08:45:30.113: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1076K, 58% free 2723K/6471K, external 595K/1042K, paused 18ms
08-19 08:45:30.143: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 43% free 3740K/6471K, external 507K/1019K, paused 19ms
08-19 08:45:35.869: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1087K, 58% free 2726K/6471K, external 595K/1019K, paused 18ms
08-19 08:45:35.899: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 43% free 3744K/6471K, external 552K/1019K, paused 18ms
08-19 08:45:39.112: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2734K/6471K, external 595K/1019K, paused 17ms
08-19 08:45:39.152: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 43% free 3752K/6471K, external 530K/1019K, paused 20ms
08-19 08:46:14.186: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1089K, 58% free 2736K/6471K, external 595K/1019K, paused 23ms
08-19 08:46:14.216: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 4K, 42% free 3755K/6471K, external 552K/1019K, paused 21ms
08-19 08:46:16.519: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1092K, 58% free 2736K/6471K, external 595K/1019K, paused 23ms
08-19 08:46:16.549: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3753K/6471K, external 530K/1019K, paused 22ms
08-19 08:47:15.686: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1089K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:47:15.716: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 3K, 42% free 3756K/6471K, external 561K/1019K, paused 18ms
08-19 08:48:01.391: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2736K/6471K, external 595K/1019K, paused 19ms
08-19 08:48:01.421: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 43% free 3753K/6471K, external 530K/1019K, paused 19ms
08-19 08:48:09.409: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1089K, 58% free 2737K/6471K, external 758K/1019K, paused 18ms
08-19 08:48:09.449: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 4K, 42% free 3756K/6471K, external 561K/1019K, paused 21ms
08-19 08:48:11.771: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1076K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:48:11.811: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3754K/6471K, external 530K/1019K, paused 20ms
08-19 08:48:13.653: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1089K, 58% free 2737K/6471K, external 595K/1019K, paused 18ms
08-19 08:48:13.683: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 3K, 42% free 3758K/6471K, external 561K/1019K, paused 19ms
08-19 08:48:15.785: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:48:15.825: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3754K/6471K, external 530K/1019K, paused 19ms
08-19 08:48:18.227: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1088K, 58% free 2737K/6471K, external 595K/1019K, paused 19ms
08-19 08:48:18.257: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 4K, 42% free 3756K/6471K, external 552K/1019K, paused 20ms
08-19 08:49:06.575: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1075K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:06.605: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3754K/6471K, external 530K/1019K, paused 17ms
08-19 08:49:09.668: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1097K, 58% free 2729K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:09.708: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 4K, 43% free 3748K/6471K, external 552K/1019K, paused 20ms
08-19 08:49:12.440: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:12.470: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 43% free 3753K/6471K, external 530K/1019K, paused 17ms
08-19 08:49:15.473: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1088K, 58% free 2736K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:15.503: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 4K, 42% free 3756K/6471K, external 561K/1019K, paused 17ms
08-19 08:49:18.476: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1091K, 58% free 2737K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:18.506: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3754K/6471K, external 507K/1019K, paused 20ms
08-19 08:49:21.289: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1090K, 58% free 2737K/6471K, external 595K/1019K, paused 18ms
08-19 08:49:21.319: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3754K/6471K, external 484K/996K, paused 20ms
08-19 08:51:43.307: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1071K, 58% free 2723K/6471K, external 595K/996K, paused 17ms
08-19 08:51:43.338: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed <1K, 43% free 3747K/6471K, external 595K/996K, paused 20ms
08-19 08:51:45.620: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1086K, 58% free 2729K/6471K, external 595K/974K, paused 18ms
08-19 08:51:45.660: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 7K, 43% free 3745K/6471K, external 462K/974K, paused 20ms
08-19 08:51:47.421: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1080K, 58% free 2738K/6471K, external 595K/974K, paused 17ms
08-19 08:51:47.452: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3755K/6471K, external 484K/974K, paused 19ms
08-19 08:52:56.949: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 27K, 58% free 2733K/6471K, external 83K/595K, paused 18ms
08-19 08:52:56.979: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed <1K, 42% free 3757K/6471K, external 83K/595K, paused 17ms
08-19 08:53:01.233: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1072K, 58% free 2727K/6471K, external 595K/1107K, paused 18ms
08-19 08:53:01.274: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 2K, 43% free 3749K/6471K, external 578K/1090K, paused 20ms
08-19 08:53:04.046: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1081K, 58% free 2740K/6471K, external 595K/1064K, paused 18ms
08-19 08:53:04.086: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3758K/6471K, external 530K/1042K, paused 20ms
08-19 08:53:25.948: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1090K, 58% free 2740K/6471K, external 595K/1042K, paused 19ms
08-19 08:53:25.978: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3758K/6471K, external 552K/1042K, paused 19ms
08-19 08:57:51.246: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1099K, 58% free 2753K/6471K, external 595K/1042K, paused 18ms
08-19 08:57:51.286: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 13K, 42% free 3764K/6471K, external 561K/1042K, paused 20ms
08-19 09:00:47.699: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1106K, 58% free 2731K/6471K, external 595K/1019K, paused 18ms
08-19 09:00:47.729: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 7K, 43% free 3748K/6471K, external 484K/996K, paused 17ms
08-19 09:00:56.817: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1080K, 58% free 2741K/6471K, external 595K/996K, paused 18ms
08-19 09:00:56.848: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3758K/6471K, external 530K/996K, paused 23ms
08-19 09:01:00.701: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2739K/6471K, external 595K/996K, paused 19ms
08-19 09:01:00.731: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3756K/6471K, external 530K/996K, paused 22ms
08-19 09:01:25.916: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 1077K, 58% free 2739K/6471K, external 595K/996K, paused 18ms
08-19 09:01:25.946: DEBUG/dalvikvm(3510): GC_FOR_MALLOC freed 6K, 42% free 3756K/6471K, external 530K/996K, paused 20ms
Horst Dehmer
  • 369
  • 1
  • 10
  • Thanks for your answer. What device have you tested it on? – Michael Aug 19 '11 at 07:45
  • I've just tested it on Google Nexus S with Android 2.3.4 and it leaks. That's quite easy to ensure if the leak exists. Initialize `junk` with `new byte[10*1204*1024]`, run `LeakActivity` and rotate a device several times. The app crashes with OOM on Nexus S after the second rotation. – Michael Aug 19 '11 at 08:33
  • HTC Sensation w/ Revolution HD 1.1.8. – Horst Dehmer Aug 20 '11 at 12:14
  • I checked the code with increased junk size (5 MBytes). No problem there. Max heap (in my case) is reported as 32 MByes (ActivityManager.getMemoryClass()). Keep in mind that GC usually needs some time to reclaim memory. What I don't know is, if the memory block has to be in one chunk, i.e. unfragmented. – Horst Dehmer Aug 20 '11 at 12:16
  • I don't know what the heap size is on HTC Sensation. Try setting `junk` size to 10 MB. When the dialog is loaded, rotate your device several times. `android:configChanges="orientation"` mustn't be set for the `LeakedActivity`. I know that it takes some time for GC to collect objects, but if you allocate such array in cycle everything will work fine. – Michael Aug 20 '11 at 12:47
  • Still not problem. Please verify in the log that on each 'flip' roughly 10 MBytes are freed. 08-20 15:46:38.944: DEBUG/dalvikvm(13335): GC_FOR_MALLOC freed 12093K, 50% free 14843K/29575K, external 0K/0K, paused 80ms 08-20 15:46:44.169: DEBUG/dalvikvm(13335): GC_FOR_MALLOC freed 12100K, 50% free 14843K/29575K, external 0K/0K, paused 68ms 08-20 15:46:53.498: DEBUG/dalvikvm(13335): GC_FOR_MALLOC freed 12099K, 50% free 14843K/29575K, external 0K/0K, paused 69ms – Horst Dehmer Aug 20 '11 at 13:54
  • Strange. I've tested it on many devices and every time I get OOM error. Unfortunately I don't have HTC Sensation to test it but thanks for your effort! – Michael Aug 20 '11 at 14:05
  • HTC Desire (2.3.4): 11 MBytes is also o.k., 12 MBytes kills the process with Out Of Memory on the first 'flip'. Nonetheless there seems to be no memory leak. I guess it's just to big a chunk of memory to be allocate frequently. – Horst Dehmer Aug 20 '11 at 14:06
  • Could you please try it with 8 MB and 3+ flips? – Michael Aug 20 '11 at 14:10
  • 1
    Done: 8 * 1024 * 1024. Changed orientation 30 times without any problem. Memory is rapidly reclaimed. Even with 2 or 3 buffers with cumulated size < 11 MBytes works fine. You might also be interested in 'Google I/O 2011: Memory management for Android Apps' [link](http://www.youtube.com/watch?v=_CruQY55HOk) – Horst Dehmer Aug 20 '11 at 14:19
  • Thanks a lot. I don't know why we have different behavior with presumable the same code. I've performed a lot of tests and I'm absolutely sure there's a memory leak. But I've never tested it on HTC and I'm going to test it out soon on some HTC devices. – Michael Aug 20 '11 at 14:30
  • To make sure, we talk the same code, check out my git repository git://github.com/dehmer/android.git. The project named 'com.frequentis.droid.alert' is the one. – Horst Dehmer Aug 20 '11 at 14:42
  • I've tested it on all HTC devices I have: Legend (2.2), Wildfire (2.2.1) and Incredible S (2.3.3). And this bug reproduces on every device. Don't know why it doesn't reproduce on your devices. Despite this @Ricky Lee gave some hints about this bug and offered a workaround. – Michael Aug 22 '11 at 11:04
0

I had run your code and when I first press the button it is showing the LeakedActivity with dialog and onClick it is removing dialog but the activity remains in foreground with black screen. On pressing back key and then again starting the activity it is showing the memory out of error exception:

ERROR/AndroidRuntime(263): java.lang.OutOfMemoryError

Then I removed the line private final byte[] junk = new byte[10*1024*1024]; from the dialog code after that no such problem exists....don't know why if anyone can put this thing in words, thanks to him/her..

I tested the code in emulator android 2.1
Vineet Shukla
  • 23,865
  • 10
  • 55
  • 63
  • Yeah, I can tell you why. That's because of the memory leak I asked about. The `junk` field was added to fill up the heap faster. It must be GC'ed after the `LeakedActivity` is finished but it's not. And that's the point. – Michael Aug 19 '11 at 11:27
-2

You need to dismiss/cancel the dialog from the onClick method as shown in the example here : Alert Dialogs in Android

Brandon Haugen
  • 961
  • 6
  • 25
  • Have you tried this? As you can see this dialog is managed by the activity and it's automatically dismissed when the activity is destroyed. – Michael Aug 17 '11 at 04:49