50

I decided it was high time I learned how to use Leak Canary to detect Leaks within my apps, and as I always do, I tried to implement it in my project to really understand how to use the tool. Implementing it was easy enough, the difficult part was reading what the tool is throwing back at me. I have a scrollview that seems to accumulate memory in the memory manager as I scroll up and down (even though It doesnt load any new data) so I thought that was a good candidate object to track leaks on, this is the result:

enter image description here

It looks like v7.widget.RecyclerView is leaking the adapter, and not my application. But that can't be right.... right?

Here's the code for the adapter and the class making use of it: https://gist.github.com/feresr/a53c7b68145d6414c40ec70b3b842f1e

I started a bounty for this question because it resurfaced after two years on a completely different application

frankelot
  • 13,666
  • 16
  • 54
  • 89
  • Looks like you're passing the application context when you should probably either use the RecyclerView's context or your activities context. Application contexts are long living which would prevent collection. – Submersed Oct 26 '17 at 15:13
  • GapWorker is the static class that performs prefetching. When the RecyclerView is destroyed, it properly unregisters itself from the GapWorker in onDetachedFromWindow(). Did you override onDetachedFromWindow() in a custom RecyclerView and forget to call super.onDetachedFromWindow() ? – BladeCoder Dec 30 '18 at 15:46

5 Answers5

85

If the adapter lives any longer than the RecyclerView does, you've got to clear the adapter reference in onDestroyView:

@Override
public void onDestroyView() {
    recyclerView.setAdapter(null);
    super.onDestroyView();
}

Otherwise the adapter is going to hold a reference to the RecyclerView which should have already gone out of memory.

Special note prior to androidx.fragment:fragment:1.3.0:

If the screen is involved in transition animations, you actually have to take this one step further and only clear the adapter when the view has become detached:

@Override
public void onDestroyView() {
    recyclerView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
            // no-op
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            recyclerView.setAdapter(null);
        }
    });
    super.onDestroyView();
}

With androidx.fragment:fragment:1.3.0 and above, onDestroyView happens after the view is detached and this extra code is no longer needed.

Brian Yencho
  • 2,748
  • 1
  • 18
  • 19
  • 4
    This is what I ended up doing, and in deed fixes the issue. I've seen this same answer in some other SO question. I would like to understand why this is the case though. The reference chain should be Fragment -> Recyclerview -> Adapter. So, if the fragment stops referencing the Recyclerview, both the recyclerview and its adapter should be garbage collected. – frankelot Oct 27 '17 at 20:26
  • 14
    When you set an adapter for a `RecyclerView` with `RecyclerView.setAdapter`, that adapter is going to register the `RecyclerView` as an `AdapterDataObserver` and hold a reference to it in an internal list. If the adapter lives longer than the `RecyclerView` (which is the case if you define the adapter in `onCreate` of a `Fragment` for example) its going to hold a reference to a `RecyclerView` that should be getting GC'ed instead. Calling `recyclerView.setAdapter(null)` unregisters the `RecyclerView` with that adapter. – Brian Yencho Oct 27 '17 at 21:01
  • what should I do if I animating screen? – vishal patel Jan 21 '19 at 12:22
  • I've run into this myself. In that case, you want to make sure you don't clear the adapter until after after the view is *detached*. – Brian Yencho Jan 21 '19 at 15:28
  • I've updated the answer to include handle the case where the screen is animating. – Brian Yencho Jan 21 '19 at 15:36
  • In my case, I used ViewBinding with autoCleared() in Fragment and I couldnt access to RecyclerView in onDestroyView method, because binding is not available at that time. I just set binding.myRv.adapter = null in onStop() override method and leaks dissappear. Thanks! – Andy Nov 11 '20 at 12:13
19

I was able to fix this by overriding RecyclerView. It happens because RecyclerView never unregisters itself from AdapterDataObservable.

@Override protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    if (getAdapter() != null) {
        setAdapter(null);
    }
}
Bolein95
  • 2,947
  • 2
  • 26
  • 31
  • This issue came back in an entirely new application, is this still the best way to solve it? – frankelot Oct 19 '17 at 19:43
  • @feresr it never went away :) I still use the same `HackyRecyclerView` since android 21 – Bolein95 Oct 19 '17 at 20:38
  • haha good to know I am not the only one facing this issue. I am currently doing onDestroyView() { recyclerView.adapter = null }. This also fixes the issue, but I would like to know what causes it in the first place. – frankelot Oct 22 '17 at 17:50
  • 4
    Unbelievable. Do we still have to patch this? Has any Google dev commented on this? – mitsest Apr 18 '19 at 12:53
  • @Bolein95, do you have kotlin solution ? – binrebin Jul 09 '20 at 10:00
6

First of all, I'm referencing this file.

It looks like v7.widget.RecyclerView is leaking the adapter, and not my application. But that can't be right.... right?

It's actually your adapter that's leaking the RecyclerView (and it's made pretty clear by the trace graph and the title of the LeakCanary activity). However, I'm not sure if it's the "parent" RecyclerView or the nested one in the HourlyViewHolder, or both. I think the culprits are your ViewHolders. By making them non-static inner classes, you explicitly provide them with the reference to the enclosing adapter class, ant this almost directly couples the adapter with the recycled views, since the parent of every itemView in your holders is the RecyclerView itself.

My first suggestion to fix this problem would be to decouple your ViewHolders and Adapter by making them static inner classes. That way they do not hold a reference to the adapter, so your context field will be inaccessible to them, and it's also a good thing, since context references should be passed and stored sparingly (also to avoid big memory leaks). When you need the context just to obtain the strings, do it somewhere else, for example in the adapter constructor, but do not store the context as a member. Finally, the DayForecastAdapter seems dangerous too: you pass the one, same instance of it to every HourlyViewHolder, which seems like a bug.

I think that fixing the design and decoupling these classes should get rid of this memory leak

maciekjanusz
  • 4,702
  • 25
  • 36
  • Well thanks!. I'm trying this out as right now!, I'll avoid passing the same DayForecastAdapter instance on every HourlyViewHolder too – frankelot Feb 21 '16 at 00:58
  • No luck, same results... If you switch to the branch ´forecastadapter-leak´ you'll see the changes I've applied. Unfortunately the error persists. Thanks for the tips though, making the VH static is a good idea. – frankelot Feb 21 '16 at 01:15
  • 1
    Looking on your branch, I really think you should create per-holder instances of the DayForecastAdapter, and in the line 157 instead of labelViewHolder.recyclerView.setAdapter(dayForecastAdapter) do something like labelViewHolder.recyclerView.adapter.setData(...) which should also call the notifyDataSetChanged() method on the adapter. Because your dayForecastAdapter is one for every hourlyViewHolder, the ForecastAdapter and dayForecastAdapter's viewHolders are coupled in a weird way, maybe that's the root of the problem? – maciekjanusz Feb 21 '16 at 13:47
  • Thanks for taking the time to actually go through the code. I will check this as soon as I get out of work today and let you know how it went. – frankelot Feb 21 '16 at 23:07
  • No luck again, for some reason the amount leak is always 196B, and the leak happens without needing to rotate the screen (scrolling through the recyclerview will do the trick), I'm a bit lost... I'll try to extract out all the ViewHolders into individual classes to make this god class a bit more readable and see if I can find the problem that way – frankelot Feb 22 '16 at 11:18
  • The weird bit is that LeakCanary is telling me I'm leaking the Adapter.... which I'm still using... How can I be leaking something I actually need... This makes me thing that maybe it is the inner adapter the one that is leaking... I'm just thikning out loud here.. tell me if this makes sesne – frankelot Feb 22 '16 at 11:32
  • Update, it turns out, I dont have to do anything. not even scrolling, the simple fact of firing up the fragment that contains ForecastAdapter will make LeakCanary cheep – frankelot Feb 22 '16 at 11:44
  • I replaced RxWeatherApplication.getRefWatcher(getContext()).watch(adapter).. for RxWeatherApplication.getRefWatcher(getContext()).watch(recyclerView);... and now.. the leaks is on the recyclerview.... I'm not trusting Leak canary as much now.. basically anything I watch so happens to have a "leak" – frankelot Feb 22 '16 at 11:49
  • That's a tough case :) Last thing I can suggest is maybe try changing to 1.4-SNAPSHOT LeakCanary and fiddle with watcher delay? It's described in the leakcanary github readme. Anyways, good luck – maciekjanusz Feb 23 '16 at 16:00
1

I can't open your image and see the actual leak but if you define a local variable for your RecyclerView in your Fragment and set your fragment's retainInstanceState true it can cause possible leaks with rotation.

When using a Fragment with retainInstance you should clear all your ui references in onDestroyView

@Override
public void onDestroyView() {
     yourRecyclerView = null;
     super.onDestroyView();
}

Here you can find a detailed information from this link: Retained Fragments with UI and memory leaks

savepopulation
  • 11,736
  • 4
  • 55
  • 80
0

So, this might be a bit of an overkill solution, but our team was getting pretty tired of having to worry about every single RecyclerView adapter leaking.

Here's an abstract RecyclerView.Adapter subclass that takes care of the leak issue once and for all.

abstract class AbstractRecyclerViewAdapter<VH : RecyclerView.ViewHolder>(fragment: Fragment) :
    RecyclerView.Adapter<VH>() {
    private val fragmentRef = WeakReference(fragment)

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        setupLifecycleObserver(recyclerView)
    }

    private fun setupLifecycleObserver(recyclerView: RecyclerView) {
        val fragment = fragmentRef.get() ?: return
        val weakThis = WeakReference(this)
        val weakRecyclerView = WeakReference(recyclerView)

        // Observe the fragment's lifecycle events in order
        // to set the Recyclerview's Adapter when the Fragment is resumed
        // and unset it when the Fragment is destroyed.
        fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                val actualRecyclerView = weakRecyclerView.get() ?: return
                when (event.targetState) {
                    Lifecycle.State.DESTROYED -> actualRecyclerView.adapter = null
                    Lifecycle.State.RESUMED -> {
                        val self = weakThis.get() ?: return
                        if (actualRecyclerView.adapter != self) {
                            actualRecyclerView.adapter = self
                        }
                    }
                    else -> {
                    }
                }
            }
        })
    }
}

Basically the Adapter observes the lifecycle events of the Fragment that contains it and takes care of setting and unsetting itself as the RecyclerView's adapter.

All you need to do is subclass AbstractRecyclerViewAdapter in your own adapters, and provide the container fragment when you construct them.

Usage:

Your adapter

class MyAdapter(fragment: Fragment): AbstractRecyclerViewAdapter<MyViewHolder>(fragment) {
    // Your usual adapter code...
}

Your fragment

class MyFragment : Fragment {
    // Instantiate with the fragment.
    private val myAdapter = MyAdapter(this)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Set the RecyclerView adapter as you would normally.
        myRecyclerView.adapter = myAdapter
    }
}
m_katsifarakis
  • 1,777
  • 1
  • 21
  • 27