Search code examples
androidkotlin-coroutinesandroid-lifecycle

Why using lifecycleScope in Fragment causes memory leak when used in ViewPager2?


I have a screen with a view pager inside which I am using multiple fragment instances of same fragment class which are using Paging 3 to load data from server.

Inside onViewCreated I have this function,

onViewCreated() {
   fun updateList()
}

fun updateList() {
       lifecycleScope.launch {
            viewModel.orders.collectLatest {
                adapter?.submitData(it)
            }
        }
}

The above function was giving me a memory leak, couldn't find a solution and gave this a try and it worked. Still not able to find why it worked or whether this is the rightway to do it.

viewLifecycleOwner.lifecycleScope.launch {
            viewModel.orders.collectLatest {
                adapter?.submitData(it)
            }
}

Leak Logcat trace:

D/LeakCanary: ​
D/LeakCanary: ┬───
D/LeakCanary: │ GC Root: Input or output parameters in native code
D/LeakCanary: │
D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
D/LeakCanary: ├─ java.lang.Object[] array
D/LeakCanary: │    Leaking: NO (InternalLeakCanary↓ is not leaking)
D/LeakCanary: │    ↓ Object[950]
D/LeakCanary: ├─ leakcanary.internal.InternalLeakCanary class
D/LeakCanary: │    Leaking: NO (HomeActivity↓ is not leaking and a class is never leaking)
D/LeakCanary: │    ↓ static InternalLeakCanary.resumedActivity
D/LeakCanary: ├─ com.awantunai.app.home.HomeActivity instance
D/LeakCanary: │    Leaking: NO (Activity#mDestroyed is false)
D/LeakCanary: │    mApplication instance of com.awantunai.app.base.AwanApplication
D/LeakCanary: │    mBase instance of androidx.appcompat.view.ContextThemeWrapper
D/LeakCanary: │    ↓ ComponentActivity.mActivityResultRegistry
D/LeakCanary: │                        ~~~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ androidx.activity.ComponentActivity$2 instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 348,5 kB in 8676 objects
D/LeakCanary: │    Anonymous subclass of androidx.activity.result.ActivityResultRegistry
D/LeakCanary: │    this$0 instance of com.awantunai.app.home.HomeActivity with mDestroyed = false
D/LeakCanary: │    ↓ ActivityResultRegistry.mKeyToLifecycleContainers
D/LeakCanary: │                             ~~~~~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ java.util.HashMap instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 504 B in 18 objects
D/LeakCanary: │    ↓ HashMap["fragment_978a66c7-ff9d-435c-a477-d49cb3df918d_rq#0"]
D/LeakCanary: │             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ androidx.activity.result.ActivityResultRegistry$LifecycleContainer instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 76 B in 3 objects
D/LeakCanary: │    ↓ ActivityResultRegistry$LifecycleContainer.mLifecycle
D/LeakCanary: │                                                ~~~~~~~~~~
D/LeakCanary: ├─ androidx.lifecycle.LifecycleRegistry instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 84,5 kB in 2074 objects
D/LeakCanary: │    ↓ Lifecycle.mInternalScopeRef
D/LeakCanary: │                ~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ java.util.concurrent.atomic.AtomicReference instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 12 B in 1 objects
D/LeakCanary: │    ↓ AtomicReference.value
D/LeakCanary: │                      ~~~~~
D/LeakCanary: ├─ androidx.lifecycle.LifecycleCoroutineScopeImpl instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,7 kB in 2042 objects
D/LeakCanary: │    ↓ LifecycleCoroutineScopeImpl.coroutineContext
D/LeakCanary: │                                  ~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ kotlin.coroutines.CombinedContext instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,7 kB in 2041 objects
D/LeakCanary: │    ↓ CombinedContext.left
D/LeakCanary: │                      ~~~~
D/LeakCanary: ├─ kotlinx.coroutines.SupervisorJobImpl instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,7 kB in 2040 objects
D/LeakCanary: │    ↓ JobSupport._state
D/LeakCanary: │                 ~~~~~~
D/LeakCanary: ├─ kotlinx.coroutines.ChildHandleNode instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,7 kB in 2039 objects
D/LeakCanary: │    ↓ ChildHandleNode.childJob
D/LeakCanary: │                      ~~~~~~~~
D/LeakCanary: ├─ kotlinx.coroutines.StandaloneCoroutine instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,6 kB in 2038 objects
D/LeakCanary: │    ↓ JobSupport._state
D/LeakCanary: │                 ~~~~~~
D/LeakCanary: ├─ kotlinx.coroutines.ChildHandleNode instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,6 kB in 2036 objects
D/LeakCanary: │    ↓ ChildHandleNode.childJob
D/LeakCanary: │                      ~~~~~~~~
D/LeakCanary: ├─ kotlinx.coroutines.internal.ScopeCoroutine instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 83,6 kB in 2035 objects
D/LeakCanary: │    ↓ ScopeCoroutine.uCont
D/LeakCanary: │                     ~~~~~
D/LeakCanary: ├─ com.awantunai.app.home.cart.v3.OrderListFragment$updateAdapter$1 instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 61,6 kB in 1246 objects
D/LeakCanary: │    Anonymous subclass of kotlin.coroutines.jvm.internal.SuspendLambda
D/LeakCanary: │    ↓ OrderListFragment$updateAdapter$1.$adapter
D/LeakCanary: │                                        ~~~~~~~~
D/LeakCanary: ├─ com.awantunai.app.home.cart.v3.OrderAdapter instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 61,6 kB in 1244 objects
D/LeakCanary: │    ↓ RecyclerView$Adapter.mObservable
D/LeakCanary: │                           ~~~~~~~~~~~
D/LeakCanary: ├─ androidx.recyclerview.widget.RecyclerView$AdapterDataObservable instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 61,0 kB in 1218 objects
D/LeakCanary: │    ↓ Observable.mObservers
D/LeakCanary: │                 ~~~~~~~~~~
D/LeakCanary: ├─ java.util.ArrayList instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 60,9 kB in 1217 objects
D/LeakCanary: │    ↓ ArrayList[0]
D/LeakCanary: │               ~~~
D/LeakCanary: ├─ androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 60,9 kB in 1215 objects
D/LeakCanary: │    ↓ RecyclerView$RecyclerViewDataObserver.this$0
D/LeakCanary: │                                            ~~~~~~
D/LeakCanary: ├─ androidx.recyclerview.widget.RecyclerView instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 60,9 kB in 1214 objects
D/LeakCanary: │    View not part of a window view hierarchy
D/LeakCanary: │    View.mAttachInfo is null (view detached)
D/LeakCanary: │    View.mID = R.id.rv_orders
D/LeakCanary: │    View.mWindowAttachCount = 1
D/LeakCanary: │    mContext instance of com.awantunai.app.home.HomeActivity with mDestroyed = false
D/LeakCanary: │    ↓ View.mParent
D/LeakCanary: │           ~~~~~~~
D/LeakCanary: ├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 57,2 kB in 1113 objects
D/LeakCanary: │    View not part of a window view hierarchy
D/LeakCanary: │    View.mAttachInfo is null (view detached)
D/LeakCanary: │    View.mID = R.id.swipe_to_refresh
D/LeakCanary: │    View.mWindowAttachCount = 1
D/LeakCanary: │    mContext instance of com.awantunai.app.home.HomeActivity with mDestroyed = false
D/LeakCanary: │    ↓ View.mParent
D/LeakCanary: │           ~~~~~~~
D/LeakCanary: ╰→ androidx.coordinatorlayout.widget.CoordinatorLayout instance
D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because com.awantunai.app.home.cart.v3.OrderListFragment received
D/LeakCanary: ​     Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
D/LeakCanary: ​     Retaining 52,8 kB in 1015 objects
D/LeakCanary: ​     key = 80c7cbe8-db9d-49d9-8607-03f6b9e662bc
D/LeakCanary: ​     watchDurationMillis = 5964
D/LeakCanary: ​     retainedDurationMillis = 949
D/LeakCanary: ​     View not part of a window view hierarchy
D/LeakCanary: ​     View.mAttachInfo is null (view detached)
D/LeakCanary: ​     View.mID = R.id.parentLayout
D/LeakCanary: ​     View.mWindowAttachCount = 1
D/LeakCanary: ​     mContext instance of com.awantunai.app.home.HomeActivity with mDestroyed = false
D/LeakCanary: 
D/LeakCanary: METADATA
D/LeakCanary: 
D/LeakCanary: Build.VERSION.SDK_INT: 32
D/LeakCanary: Build.MANUFACTURER: Google
D/LeakCanary: LeakCanary version: 2.8.1
D/LeakCanary: App process name: com.awantunai.app.alpha
D/LeakCanary: Stats: LruCache[maxSize=3000,hits=134631,misses=245082,hitRate=35%]
D/LeakCanary: RandomAccess[bytes=14017705,reads=245082,travel=175631031479,range=69026744,size=98758092]
D/LeakCanary: Analysis duration: 25897 ms

Solution

  • Did some research and realized that viewLifeCycleOwner.lifecycleScope is bound to lifecycle starting from onCreateView to onDestroyView.

    On the other hand, if we use just use this.lifecycleScope, this scope will be bound starting from onAttach to onDetach.

    In the above expression, let's say the view got destroyed when i moved to the other fragment and after this before calling onDetach i get response in

    viewModel.orders.collectLatest {
    adapter?.submitData(it)
    }
    

    This will access recyclerview when fragment's view is destroyed because its holding reference to adapter and cause a memory leak

    Here is some reference: https://cs-ibrahimyilmaz.medium.com/viewlifecycleowner-vs-this-a8259800367b