尝试检测ActionMode内存泄漏

11

我已经在连续几天尝试找到ActionMode内存泄漏的源头,但一直没能成功。我的一个Activity中有多个Fragment,当我离开包含ActionMode的Fragment(同时自动取消它),LeakCanary就会检测到内存泄漏。

我已经在destroy()中将ActionMode和ActionMode.Callback都置为null,甚至尝试在onDestroyActionMode()中这样做。

以下是我的LeakCanary截图:

https://i.imgur.com/RUbdqj3.png

希望有人能指引我正确的方向。

P.S. 我怀疑问题可能与ActionMode.Callback有关。虽然我无法找到任何销毁它的方法,我使用startSupportActionMode(mActionModeCallback)启动ActionMode。我也尝试找到从中删除mActionModeCallback的方法,但没有找到任何方法。

以下是完整的ActionMode代码:

private ActionMode mActionMode;
private ActionMode.Callback mActionModeCallback;

public void startCAB()
{
    if (mActionMode == null)
        mActionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(mActionModeCallback);
}


private void buildActionModeCallBack()
{
    mActionModeCallback = new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.menu_cab, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                ... Some Code ...
            }
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mActionMode = null;
    mActionModeCallback = null; // Tried with and without this.
        }
    };
}

public void finishActionMode()
{
    mActionMode.finish();
}

@Override
public void onDestroy()
{
    super.onDestroy();
    mActionMode = null;
    mActionModeCallback = null;
}

包含片段的父活动:

@Override
public void onTabUnselected(TabLayout.Tab tab)
{
    clearCAB();
}

private void clearCAB()
{
    int index = mPagerAdapter.getCurrentFragmentIndex();
    FragmentOne fragmentOne = (FragmentOne) mPagerAdapter.instantiateItem(mViewPager, index);
    fragmentOne.finishActionMode();
}

你尝试过调用 mActionMode.finish() 而不是显式地将引用设置为 null 吗? - earthw0rmjim
是的,我已经添加了来自我的父活动的代码,它显示我在滑动时完成CAB的位置。 - Jack
在我的应用程序中遇到了同样的问题,它使用了一个嵌套在视图页中的片段内的列表视图。 - mliu
在看到这个问题之前,我正在查看源代码,因为它似乎很可疑,我无法取消ActionMode.Callback。一旦调用了AppCompatActivity.startSupportActionMode(mActionModeCallback),就再也没有取消引用ActionMode.Callback,因此回调始终具有强引用。解决方案是创建一个具体的回调实现,其生命周期与应用程序相同,然后自己设置/取消自定义回调实现中的变量,这样整个片段就不会泄漏 - 这是aosp的错误。 - Mark
在这篇文章发表一年半之后,我也遇到了完全相同的问题。你找到解决方案了吗? - or_dvir
3个回答

4

根据我的经验,如果你的 ActionMode.Callback 对象使用匿名内部类,可能会导致片段内存泄漏。

也许你可以创建一个新类并实现 ActionMode.Callback 然后将其用于放置在 startSupportActionMode() 的参数中:

public class YourFragment extends skip implements skip, ActionMode.Callback {

    private ActionMode mActionMode;

    public void startCAB()
    {
        if (mActionMode == null)
            mActionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(new SafeActionModeCallback(this));
    }

    public void finishActionMode()
    {
        mActionMode.finish();
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.menu_cab, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return false;
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
            // ... Some Code ...
        }
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        mActionMode = null;
    }
}

SafeActionModeCallback:

public class SafeActionModeCallback implements ActionMode.Callback {

    // you can also use the WeakReference
    private ActionMode.Callback callback;

    public SafeActionModeCallback(ActionMode.Callback callback) {
        this.callback = callback;
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        return callback.onCreateActionMode(mode, menu);
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return callback.onPrepareActionMode(mode, menu);
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        return callback.onActionItemClicked(mode, item);
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        callback.onDestroyActionMode(mode);
        callback = null;
    }
}

我尝试了类似的东西,但是我使用了lambda表达式(kotlin)代替ActionMode.Callback变量,但这并没有起作用 - fragment仍然泄漏。 - or_dvir
绝对适用于我,泄漏已经解决了。 - Nicolas

1

看起来活动中的ActionMode引用了片段的布局,导致内存泄漏并防止片段被GC回收。我找不到删除引用的方法。

在我的使用情况中,我在片段中使用了一个ListView,该ListView通过listener.setMultiChoiceModeListener激活了活动的ActionMode。

我的hacky解决方案:在片段的onDestroyView中,从布局中删除listView(或激活ActionMode的任何视图),并删除listView的所有侦听器。我为此编写了一个kotlin扩展方法:

fun ListView.removeViewAndClearListeners() {
    setMultiChoiceModeListener(null)
    setOnScrollListener(null)
    onItemClickListener = null

    (parent as? ViewGroup)?.removeView(this)
}

做完这个之后,漏洞就消失了。

已激活意味着操作模式处于“活动”状态,并在菜单中显示上下文按钮。 - mliu
对我来说很明显,因为我将操作模式附加到了列表视图上。如果你不知道,也许可以通过编程或者添加断点来找到它。 - mliu
你用什么命令来粘贴它呢?感谢您的帮助。 - Jack
listView.setMultiChoiceModeListener(...) - mliu
@milu,非常抱歉回复晚了。遗憾的是,ListView与RecyclerView不同。对于ListView,您可以使用setMultiChoiceModeListener()之类的方法。RecyclerView没有类似的方法。您可以直接启动ActionMode。我只需从适配器传递一个调用到片段,然后启动ActionMode。非常感谢您抽出时间来帮助我。我真的很感激。 - Jack
显示剩余3条评论

-1

我仍然在想为什么你要依赖ActionMode.Callback。我有一个应用程序,需要在长按时创建自定义菜单,我在这个问题上浪费了将近两个月的时间:

ActionModeCallback不起作用

我不确定你是否知道这一点,ActionMode回调在所有设备上都几乎无法正常工作。经过大量研究,我得知那些专注于电池消耗和优化的设备将不会让您的后台服务和某些回调按预期工作。

尝试在小米或Oppo/Vivo设备上测试代码。它将直接跳转到 onDestroyActionMode 而不是调用 onActionItemClicked


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接