Memory leak with fragments

I am developing an application with single activity. There is main fragment and two fragments inside, here is the code:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="viewModel"
            type="com.example.coreproject.presentation.viewModel.MainViewModel" />
    </data>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_horizontal"
            android:orientation="vertical">

            <Button
                android:id="@+id/back_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/padding_normal"
                android:onClick="@{viewModel::resetAuth}"
                android:text="Reset authorization" />

            <Button
                android:id="@+id/about_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/padding_normal"
                android:onClick="@{viewModel::goToAbout}"
                android:text="About" />

            <fragment
                class="com.example.services.presentation.view.PopularServiceListFragmentWidget"
                android:id="@+id/widget_popular_services"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/padding_normal"/>

            <fragment
                class="com.example.services.presentation.view.CategoryListFragmentWidget"
                android:id="@+id/widget_categories"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/padding_normal"
                android:layout_marginBottom="@dimen/padding_big"/>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

</layout>

I can go to another fragment from here, when I do this, a memory leak occurs. Here is the stack trace:

LeakingInstance(referenceKey=c4b8a77a-e8c0-4e6a-937d-d14422c9a616, referenceName=, 
instanceClassName=androidx.core.widget.NestedScrollView, watchDurationMillis=-515750270, 
exclusionStatus=null, leakTrace=
D/LeakCanary: ┬
D/LeakCanary: ├─ android.os.HandlerThread
D/LeakCanary: │    Leaking: NO (it's a GC root)
D/LeakCanary: │    Thread name: 'LeakCanary-Heap-Dump'
D/LeakCanary: │    ↓ thread HandlerThread.contextClassLoader
D/LeakCanary: ├─ dalvik.system.PathClassLoader
D/LeakCanary: │    Leaking: NO (Object[]↓ is not leaking and Classloader never leaking)
D/LeakCanary: │    ↓ PathClassLoader.runtimeInternalObjects
D/LeakCanary: ├─ java.lang.Object[]
D/LeakCanary: │    Leaking: NO (Glide↓ is not leaking)
D/LeakCanary: │    ↓ array Object[].[694]
D/LeakCanary: ├─ com.bumptech.glide.Glide
D/LeakCanary: │    Leaking: NO (Glide↓ is not leaking and a class is never leaking)
D/LeakCanary: │    ↓ static Glide.glide
D/LeakCanary: ├─ com.bumptech.glide.Glide
D/LeakCanary: │    Leaking: NO (ArrayList↓ is not leaking)
D/LeakCanary: │    ↓ Glide.managers
D/LeakCanary: ├─ java.util.ArrayList
D/LeakCanary: │    Leaking: NO (Object[]↓ is not leaking)
D/LeakCanary: │    ↓ ArrayList.elementData
D/LeakCanary: ├─ java.lang.Object[]
D/LeakCanary: │    Leaking: NO (RequestManager↓ is not leaking)
D/LeakCanary: │    ↓ array Object[].[2]
D/LeakCanary: ├─ com.bumptech.glide.RequestManager
D/LeakCanary: │    Leaking: NO (SupportRequestManagerFragment$SupportFragmentRequestManagerTreeNode↓ is not leaking)
D/LeakCanary: │    ↓ RequestManager.treeNode
D/LeakCanary: ├─ com.bumptech.glide.manager.SupportRequestManagerFragment$SupportFragmentRequestManagerTreeNode
D/LeakCanary: │    Leaking: NO (SupportRequestManagerFragment↓ is not leaking)
D/LeakCanary: │    ↓ SupportRequestManagerFragment$SupportFragmentRequestManagerTreeNode.this$0
D/LeakCanary: ├─ com.bumptech.glide.manager.SupportRequestManagerFragment
D/LeakCanary: │    Leaking: NO (MainFragment↓ is not leaking and SupportRequestManagerFragment#mFragmentManager is not null)
D/LeakCanary: │    ↓ SupportRequestManagerFragment.mParentFragment
D/LeakCanary: ├─ com.example.coreproject.presentation.view.MainFragment
D/LeakCanary: │    Leaking: NO (MainFragment#mFragmentManager is not null)
D/LeakCanary: │    ↓ MainFragment.binding
D/LeakCanary: │                   ~~~~~~~
D/LeakCanary: ├─ com.example.coreproject.databinding.FragmentMainBindingImpl
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    ↓ FragmentMainBindingImpl.mRoot
D/LeakCanary: │                              ~~~~~
D/LeakCanary: ╰→ androidx.core.widget.NestedScrollView
D/LeakCanary: ​     Leaking: YES (RefWatcher was watching this)
D/LeakCanary: , retainedHeapSize=null)])

In addition to the main fragment, the built-in and all subsequent ones also leak if you switch to them, but if you return to the main one, the leak disappears. I have no idea how to fix this. Any help is appreciated.

Upvotes: 0

Views: 1720

Answers (2)

Bacar Pereira
Bacar Pereira

Reputation: 1145

According to google documents View Binding fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.

// This property is only valid between onCreateView and
// onDestroyView.
private var _binding: LoginBinding? = null
private val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 
  savedInstanceState: Bundle?
): View? {
   _binding = LoginBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}

override fun onDestroyView() {
  super.onDestroyView()
  _binding = null
}

You can now use the instance of the binding class to reference any of the views:

Upvotes: 1

Pierre-Yves Ricau
Pierre-Yves Ricau

Reputation: 8349

Based on the leaktrace, we can see that MainFragment is not leaking but it has a binding field which has a reference to FragmentMainBindingImpl which itself references a detached view. That view is the view of MainFragment, which means that Fragment.onDestroyView() was called. When Fragment.onDestroyView() is called then the fragment should let go of the reference to the view, ie binding should be set to null.

Upvotes: 3

Related Questions