Android:AlertDialog导致内存泄漏

Mic*_*ael 14 android memory-leaks android-alertdialog

我的应用程序显示了AlertDialog一个ListView内部.一切都工作得很好然后我决定测试这个内存泄漏.运行应用程序一段时间后,我打开了MAT并生成了Leak Suspects报告.MAT发现了几个类似的泄漏:

"<system class loader>"加载的"com.android.internal.app.AlertController $ RecycleListView"的一个实例占用...

我花了很多时间寻找泄漏的原因.代码审查没有帮助我,我开始谷歌搜索.这就是我发现的:

问题5054:AlertDialog似乎通过MessageQueue中的Message导致内存泄漏

我决定检查这个bug是否重现.为此我创建了一个由两个活动组成的小程序.MainActivity是一个enrty点.它只包含一个运行的按钮LeakedActivity.后者只是AlertDialog在其onCreate()方法中显示了一个.这是代码:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

MAT报告此应用程序com.android.internal.app.AlertController$RecycleListView每次AlertDialog被解雇并LeakedActivity完成时都会泄漏.

我在这个小程序中找不到任何错误.它看起来像一个非常简单的使用情况,AlertDialog它必须运作良好,但似乎没有.因此,我想知道如何在使用AlertDialog带项目的s 时避免内存泄漏.为什么这个问题没有修复呢?提前致谢.

Ric*_*Lee 27

(2012年12月12日):见下面的更新.

这个问题实际上并不是由AlertDialog与之相关的问题引起的ListView.您可以使用以下活动重现相同的问题:

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"};
}
Run Code Online (Sandbox Code Playgroud)

旋转设备几次,你就会得到OOM.

我没有时间去研究更多关于真正原因事情(我知道发生了什么但不清楚它为什么会发生;可能是bug,或者是设计的).但是这是你可以做的一种解决方法,至少在你的情况下避免使用OOM.

首先,您需要保留对泄露的参考AlertDialog.你可以在这里做到这一点onCreateDialog().当你使用时setItems(),AlertDialog遗嘱将在内部创建一个ListView.当您onClickListener()setItems()通话中设置时,它将在内部分配给ListView onItemClickListener().

然后,在泄漏的活动中onDestroy(),将AlertDialog's ListView' 设置onItemClickListener()null,这将释放对侦听器的引用,使得在该侦听器内分配的任何内存都符合GC的条件.这样你就不会得到OOM.这只是一种解决方法,真正的解决方案应该实际应用于ListView.

以下是您的示例代码onDestroy():

@Override
protected void onDestroy() {
    super.onDestroy();
    if(leakedDialog != null) {
            ListView lv = leakedDialog.getListView();
            if(lv != null)  lv.setOnItemClickListener(null);
    }
}
Run Code Online (Sandbox Code Playgroud)

UPDATE(2012/2/12):经过进一步调查,这个问题其实并没有特别涉及到ListView也不是OnItemClickListener,但这样的事实,GC不会立即发生,需要时间来决定哪些对象有资格和准备GC.试试这个:

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
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

旋转几次,你就会得到OOM.问题是你旋转之后junk仍然保留,因为GC还没有,也不会发生(如果你使用MAT,你会看到这junk仍然是由GCroot深处的按钮的监听器保留的,它会需要时间让GC确定这junk是否合格并且可以GCed.)但同时,junk需要在旋转后创建一个新的,并且由于mem alloc大小(每个垃圾10M),这将导致OOM .

解决方案是打破对侦听器(junk所有者)的任何引用,在这种情况下是从按钮,这实际上使侦听器成为GCroot,只有短路径到垃圾,并使GC决定更快地回收垃圾内存.这可以在以下方面完成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();
}
Run Code Online (Sandbox Code Playgroud)

  • 当然是.我的观点是,在stackoverflow中有很多问题都有很好的答案,但没有标记为已回答(答案未被接受).如果每个要求有点责任并标记/接受答案的人对社区有好处,这对于有类似问题的其他人会更有帮助. (2认同)