Tracing simple memory leak around RecyclerView using LeakCanary
Figuring out the object references under the hood and the importance of LeakCanary
Preface
This article is mostly meant for novice to mid level Android programmers, who haven’t really digged into LeakCanary yet. I myself used it for the first time recently after delving into Android development for a year. And I am pleasantly surprised how powerful this tool is. This is definitely a must-include tool in every project. At the same time, I was surprised how Android maintains references under the hood for RecyclerView
s. With naive expectation that RecyclerView
itself should avoid circular references, you can easily fall into a trap of memory leaks. (And that's exactly the kind of reason that Square guys implemented LeakCanary and everybody should use it)
How to use LeakCanary
It’s pretty simple to use LeakCanary. As instructed in the README section, you just need to 1. describe dependency in gradle and 2. write a few lines in your Application
subclass. And then LeakCanary will alert you of the memory leak in your debug build.
However, as straight-forward as it sounds, there was one pitfall I got into. If you are like me and prefers to press Debug button instead of Run button on Android Studio, LeakCanary doesn’t run while you are debugging. You have to stop the debugging, and start the installed debug build from the launcher.
I have summarized this flow into a video, if this helps :
A case you can easily produce memory leak
Let’s look at a case where I was quite surprised to cause memory leak. The basic structure of the code looks like this :

Fragment
shows RecyclerView
and it's adapter
provides custom Viewholder
s. As simple as it can be, right? One thing that deviates from simplest structure is that the Fragment
keeps reference to the adapter
. This reference is meant to reuse adapter
even after Activity
is refreshed due to rotation etc. We are showing RecyclerView
on top of the Fragment
, so I think it is a sensible option to match the lifetime of RecyclerView
's adapter
to the one of the Fragment
as opposed to the Activity
.
The corresponding part of the code looks as follows :
This structure looks memory leak safe because there doesn’t seem any circular references. However, my naiive expectation is false...
The object reference path provided by LeakCanary looks like this:

To my surprise, this diagram tells me that RecyclerView.mAdapter
holds an indirect reference to MainActivity
through RecyclerView.mContext
. This is not a reference we made ourselves. This is a “hidden” reference, if we may call it.
So, the actual structure with this “hidden” reference (indicated by the dashed lines) is something like the next diagram.

You can see there is a beautiful circular reference from MainFragment
=> MainRecyclerViewAdapter
=> RecyclerView
=> MainActivity
=> MainFragment
and so on... Rotation happens, and MainActivity
gets recreated, but since MainFragment
still lives after rotation and keeps indirect reference to the old MainActivity
, the old MainActivity
will never reclaimed by GC and leaks.
As a side note, the RecyclerView
is always recreated after rotation and reference from MainFragment
to the old RecyclerView
through Android-Kotlin extension never stays after rotation (indicated by the red cross in the diagram). That's how Android works.
Solution 1
A simple solution is to shorten the lifetime of adapter
to match with the one of the Activity
. Showing only the diff of before-after in the sample code below.
Every time when rotation happens, you will ditch the oldadapter
that holds an indirect reference to the old Activity
.
If we look at the structure, we don’t have the circular reference anymore, because we removed link from Fragment
to adapter
.

The cons of this approach is that you cannot save the temporary state in the adapter
, because the adapter
is initialized at every rotation. We have to save the temporary state somewhere else, and let the adapter
to fetch the state after every initialization.
Solution 2
Another simple solution is to call recyclerView.adapter = null
from onDestroyView
.
Actually, I was surprised that this approach works. Even if you null out the reference from RecyclerView
to adapter
, as long as the adapter
has a reference to RecyclerView
, you still have circular reference. The only way I can comprehend is that Android actually nulls out the reference from adapter
to the RecyclerView
as well when you null out the reverse reference, thereby eliminating the circular reference entirely.

Summary
Even though I think solution 1 is by-the-book approach, it has a shortcoming that you can not let adapter
to hold temporary status. If you need adapter
to maintain temporary status, then probably better to pick solution 2.
In any case, you want to prepare your mental model including the “hidden” references, in order to flexibly handle such memory leak situation. And LeakCanary can really help you shaping this mental model. Otherwise, it was impossible for me to know that there is such hidden references around RecyclerView
without reading the internal code.
If you are interested, I put the sample code in GitHub. You can follow git tags to get different stage of the code. (adapter-memory-leak tag shows the code that causes memory leak, fix-adapter-memory-leak-1 tag shows the solution 1 to treat the memory leak, etc.)
Another interesting point I want to note is that this type of memory leak does not occur with ViewPager
. Your Fragment
can hold reference to ViewPager.adapter
and it causes no memory leak. The way the ViewPager
set "hidden" references should be a bit different from how RecyclerView
does.
That is it. Bye bye memory leaks. Long live LeakCanary!!