Please quote the author and source

The background that

To make the problem clearer, I simplified and abstracted the scenario in which the problem occurred. You now have an Activity whose body is a ListView. The ListView contains multiple modules, each corresponding to its own view. Each module implements a Section interface:

public interface Section {
    public View getView(int position, View convertView, ViewGroup parent);
}
Copy the code

The getView of the ListView adapter calls the getView of each Section to get the views of different modules.

Now there is a module TestSection corresponding to the view is a horizontal RecyclerView, the core code is as follows:

public class TestSection implements Section {
    RecyclerView mRecyclerView;
    public View getView(int position, View convertView, ViewGroup parent) {
        if (mRecyclerView == null) {
            mRecyclerView = new RecyclerView(parent.getContext());
            mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false));
        }
        returnmRecyclerView; }}Copy the code

ListView supports drop-down refresh. After the refresh, the ListView clears all the sections and then creates a new Section collection based on the new data. The memory leak appears right after the pull-down flush!

LeakCanary has this to say:

  • org.chromium.base.SystemMessageHandler.mLooper
  • references android.os.Looper.mThread
  • references thread java.lang.Thread.localValues (named ‘main’)
  • references java.lang.ThreadLocal$Values.table
  • references array java.lang.Object[].[31]
  • references android.support.v7.widget.GapWorker.mRecyclerViews
  • references java.util.ArrayList.array
  • references array java.lang.Object[].[23]
  • references android.support.v7.widget.RecyclerView.mContext
  • references com.test.TestActivity

To explore problems

LeakCanary message, there is a good place to start is android. Support. V7. Widget. GapWorker. MRecyclerViews. Let’s see who this GapWorker is.

final class GapWorker implements Runnable {

    static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();

    ArrayList<RecyclerView> mRecyclerViews = newArrayList<>(); . }Copy the code

There are two key members of GapWorker, sGapWorker and mRecyclerViews. According to LeakCanary, it is this mRecyclerViews that references the mRecyclerView in the TestSection that is causing the memory leak. Note that the sGapWorker is static. It can be inferred that the static sGapWorker refers to an instance of GapWorker. The mRecyclerViews in the GapWorker instance references the mRecyclerView in the TestSection, causing a memory leak. Next, we need to find the connection between GapWorker and RecyclerView. The key codes are as follows:

public class RecyclerView extends ViewGroup implements ScrollingView.NestedScrollingChild {... GapWorker mGapWorker; .private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21; .@Override
    protected void onAttachedToWindow(a) {...if (ALLOW_THREAD_GAP_WORK) {
            // Register with gap worker
            mGapWorker = GapWorker.sGapWorker.get();
            if (mGapWorker == null) {
                mGapWorker = newGapWorker(); . GapWorker.sGapWorker.set(mGapWorker); } mGapWorker.add(this); }}@Override
    protected void onDetachedFromWindow(a) {...if (ALLOW_THREAD_GAP_WORK) {
            // Unregister with gap worker
            mGapWorker.remove(this);
            mGapWorker = null; }}}Copy the code

You can see that there is a member variable mGapWorker of GapWorker type in RecyclerView. The mGapWorker actually references a global Instance of GapWorker. In onAttachedToWindow RecyclerView adds itself to the mRecyclerViews list of the global GapWorker instance, and in onDetachedFromWindow RecyclerView removes itself from the global list. So if you have onAttachedToWindow you should have onDetachedFromWindow, but now it looks like the problem is that onDetachedFromWindow is not being called.

To get to the bottom of the problem, let’s go back to our current application scenario. The ListView pulls down the refresh to clear all sections and then creates a new Section collection. This means that a new TestSection instance is created. Let’s look at the TestSection getView implementation:

public class TestSection implements Section {
    RecyclerView mRecyclerView;
    public View getView(int position, View convertView, ViewGroup parent) {
        if (mRecyclerView == null) {
            mRecyclerView = new RecyclerView(parent.getContext());
            mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false));
        }
        returnmRecyclerView; }}Copy the code

When the getView of this new TestSection instance is called for the first time, mRecyclerView is null. Because of ListView reuse, the argument convertView is not null. Instead, it refers to the mRecyclerView of the previous TestSection instance! So now there are two recyclerViews, we will call the new mRecyclerView NewRV, the original mRecyclerView called OldRV. After getView returns, NewRV becomes a child of the ListView, and its onDetachedFromWindow is called normally. OldRV, however, became a wild child, with no one calling its onDetachedFromWindow. So it sits quietly in the mRecyclerViews list of the global GapWorker instance, innocently leaking the entire Activity!

The solution

Now that you’ve got to the bottom of the problem, the solution is clear — just reuse convertView correctly.

public class TestSection implements Section {
    RecyclerView mRecyclerView;
    public View getView(int position, View convertView, ViewGroup parent) {
        if (mRecyclerView == null) {
            if (convertView instanceof RecyclerView) {
                mRecyclerView = (RecyclerView) convertView;
            } else {
                mRecyclerView = new RecyclerView(parent.getContext());
                mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false)); }}returnmRecyclerView; }}Copy the code

After the ListView nesting RecyclerView really need to be careful of memory leaks!