Trying to detect ActionMode memory leak

Viewed 750

I have been trying to find the source of ActionMode memory leak for days now without luck. I have an activity with several fragments and when I leave the fragment having ActionMode (while auto cancelling it), LeakCanary detects a memory leak.

I have nulled both ActionMode and ActionMode.Callback on destroy() and even tried doing it on onDestroyActionMode().

Here is my LeakCanary screenshot:

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

I hope someone points me in the right direction.

P.S. I have suspected it has something to do with ActionMode.Callback. Though, I could not find any methods for the CallBack that destroys it. I start the ActionMode using startSupportActionMode(mActionModeCallback). I have tried to find a method to remove the mActionModeCallback from that, too, but no methods.

Here is my full ActionMode code:

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;
}

Parent Activity containing fragments:

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

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

According to my experience, if your ActionMode.Callback object use the Anonymous inner class it may cause your fragment memory leak.

Maybe you can create a new class and implements ActionMode.Callback then use it to put in startSupportActionMode() parameter:

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;
    }
}

It seems the ActionMode in the activity has a reference to the fragment's layout which is causing the memory leak and preventing the fragment from getting GC'ed. I couldn't find a way to remove the reference.

In my use case, I'm using a ListView inside the fragment that was activating the activity's ActionMode (via listener.setMultiChoiceModeListener).

My hacky solution: In the fragment's onDestroyView, remove the listView (or whichever view activated the ActionMode) from the layout and remove all listeners for the list view. I made a kotlin extension method for it:

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

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

After doing this, the leak is gone.

I am still wondering why you are relying on ActionMode.Callback. I had an application where I was supposed to create a Custom Menu on long press and I wasted almost 2 months on this issue :

ActionModeCallback does not work

I am not sure If you are aware of this or not, The ActionMode Callback barely works on all devices. After a lot of research, I came to know that devices who are focusing too much on battery consumption and optimization will not let your background services and some callbacks work as expected.

Try testing your code on MI or Oppo/Vivo devices. It will jump directly to onDestroyActionMode instead of calling onActionItemClicked

Related