The 5 Most Common Android Memory Leaks

PINAR TURGUT
5 min readJul 15, 2023

--

Since Fragments entered our development world, they made some of our job simpler. With fragments, it is now easier to divide the mobile screen into two pieces. Or, another good example from enter/exit animations when navigating another page is much more efficient than working with activities. However, fragments also brought more lifecycle complexities on top of already complex activity lifecycles. Unfortunately, with more lifecycles to keep in mind would mean the more risk for memory leaks to sneak to the production. Therefore, in this article, we will try to cover the 5 most common android memory leaks might occur due to lifecycle complexities.

Table of Contents

What is the Memory Leak?

Before we get into any details of the memory leaks, it is a good practice to understand how they happen and what we should do to avoid them. In Android projects, we have a helper assistant called garbage collector. Garbage collector frequently looks for unused objects to clean up from the memory. With our great assistant, we can have a quite efficient app with very less effort.

Additionally, in android projects, the object/process allocates largest space in the memory is the View itself. So, our great assistant will always look for to clean up any unused View. With that being said, it is easier to imagine that a memory leak can occur if garbage collector cannot clean up the View allocation from the memory. This happens usually if there is another object holds a strong reference to the View that garage collector is trying to clean up.

1. ComposeView Memory Leaks

At first, this kind of memory leak is challenging to spot on. With the help of Leak Canary, it can be easily seen what is wrong. You might be thinking that ComposeView is a View. How can a View leak within another View? Should not it be destroyed when onDestroy (or in the case of fragments onDestroyView) is called?

You guessed right. There are some scenarios that ComposeView might leak when it is inside a fragment. Fragments can receive onDestroyView even though the parent activity is still on onResume. A couple example into this, usage of BottomNavigationView, or navigating one fragment to another using back stack entry. Both of the scenarios, you might choose to replace the existing fragment, but you might also want to keep previous fragment in the back stack so that the user can navigate back when they click back button.

When that is the case, it is a suggested practice that we as android developers need to encounter that the user is going to be back to the previous fragment. And, the android system is going to create another ComposeView as in its nature. However, compose works different than normal View as we know for the fragments. ComposeView’s content is attached to the activity’s View rather than fragment’s View. Technically speaking, fragments are not a View and they only have a parent as a host.

So, we need to make sure that we clear the ComposeView content before the user navigates back. Before jumping in the code, I would like to show an example log from leak canary.

┬───
│ GC Root: System class

├─ android.view.WindowManagerGlobal class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static WindowManagerGlobal.sDefaultWindowManager
├─ android.view.WindowManagerGlobal instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ WindowManagerGlobal.mRoots
├─ java.util.ArrayList instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayList[1]
├─ android.view.ViewRootImpl instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ mContext instance of com.android.example.MainActivity with mDestroyed = false
│ ViewRootImpl#mView is not null
│ mWindowAttributes.mTitle = "Toast"
│ mWindowAttributes.type = 2005 (Toast)
│ ↓ ViewRootImpl.mContext
├─ com.android.example.MainActivity instance
│ Leaking: NO (MainFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.android.example.MyApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ ComponentActivity.mOnConfigurationChangedListeners
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: NO (MainFragment↓ is not leaking)
│ ↓ CopyOnWriteArrayList[3]
├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
│ Leaking: NO (MainFragment↓ is not leaking)
│ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (MainFragment↓ is not leaking)
│ ↓ FragmentManager.mParent
├─ com.android.example.MainFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ Fragment.mTag=Home
│ ↓ MainFragment.binding
│ ~~~~~~~
├─ com.android.example.databinding.FragmentMainBindingImpl instance
│ Leaking: UNKNOWN
│ Retaining 2.9 MB in 7184 objects
│ ↓ FragmentMainBinding.statusBanner
│ ~~~~~~~~~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 20.9 kB in 443 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.main_container
│ View.mWindowAttachCount = 1
│ mContext instance of com.android.example.MainActivity with mDestroyed = false
│ ↓ FrameLayout.mMatchParentChildren
│ ~~~~~~~~~~~~~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ Retaining 19.3 kB in 424 objects
│ ↓ ArrayList[0]
│ ~~~
╰→ androidx.compose.ui.platform.ComposeView instance
Leaking: YES (ObjectWatcher was watching this because com.android.example.MainFragment received
Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
Retaining 19.3 kB in 422 objects
key = 8eb552d8-57ad-441d-805a-ec283035865f
watchDurationMillis = 5604
retainedDurationMillis = 603
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of com.android.example.MainActivity with mDestroyed = fals

It might look complex to some of us, but the thing that we need to look for is that what the leak is. As you can see in the above, the LeakCanary point that ComposeView is leaking for some reason. And, if you follow the chain upwards, you will notice that Activity is referenced before the fragment. Which means that, when the user navigates away from the fragment, then ComposeView leaks. This happens because android garbage collector is trying to destroy fragment’s view; however, the view reference is hold by the host activity itself. In this case, the garbage collector is unable to remove the view when the fragment receives onDestroyView event. And, this causes the ComposeView to be leaked.

Rest of the blog on my own website please visit https://pinartechtips.com/5-most-common-android-memory-leaks/

Happy Coding.

--

--