app memory leaks likely related to Jetpack Compose and Paging

Viewed 803

I recently converted parts of my project to Jetpack Compose I show a list of text items using LazyColumn with data supplied through Pager. But I keep getting lots of warnings from LeakCanary about memory leaks. Seems they are related to the paged data, but I don't how to release them when activities are destroyed. Any help please?

D/LeakCanary: ====================================
D/LeakCanary: HEAP ANALYSIS RESULT
D/LeakCanary: ====================================
D/LeakCanary: 2 APPLICATION LEAKS
D/LeakCanary: 
D/LeakCanary: References underlined with "~~~" are likely causes.
D/LeakCanary: Learn more at https://squ.re/leaks.
D/LeakCanary: 
D/LeakCanary: 2607730 bytes retained by leaking objects
D/LeakCanary: Signature: 217fb7a74f0f705a023f24a3e73a47b2525897d7
D/LeakCanary: ┬───
D/LeakCanary: │ GC Root: Thread object
D/LeakCanary: │
D/LeakCanary: ├─ android.os.HandlerThread instance
D/LeakCanary: │    Leaking: NO (PathClassLoader↓ is not leaking)
D/LeakCanary: │    Thread name: 'LeakCanary-Heap-Dump'
D/LeakCanary: │    ↓ Thread.contextClassLoader
D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking and A ClassLoader is never leaking)
D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
D/LeakCanary: ├─ java.lang.Object[] array
D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking)
D/LeakCanary: │    ↓ Object[3439]
D/LeakCanary: ├─ androidx.compose.runtime.snapshots.SnapshotKt class
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking and a class is never leaking)
D/LeakCanary: │    ↓ static SnapshotKt.applyObservers
D/LeakCanary: ├─ java.util.ArrayList instance
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
D/LeakCanary: │    ↓ ArrayList[0]
D/LeakCanary: ├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
D/LeakCanary: │    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
D/LeakCanary: ├─ androidx.compose.runtime.Recomposer instance
D/LeakCanary: │    Leaking: NO (Recomposer is in state PendingWork)
D/LeakCanary: │    ↓ Recomposer.snapshotInvalidations
D/LeakCanary: │                 ~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ java.util.ArrayList instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 5.8 MB in 32399 objects
D/LeakCanary: │    ↓ ArrayList[8]
D/LeakCanary: │               ~~~
D/LeakCanary: ├─ java.util.HashSet instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 176 B in 6 objects
D/LeakCanary: │    ↓ HashSet[element()]
D/LeakCanary: │             ~~~~~~~~~~~
D/LeakCanary: ├─ androidx.compose.runtime.ParcelableSnapshotMutableState instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 56 B in 3 objects
D/LeakCanary: │    ↓ SnapshotMutableStateImpl.next
D/LeakCanary: │                               ~~~~
D/LeakCanary: ├─ androidx.compose.runtime.SnapshotMutableStateImpl$StateStateRecord instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 40 B in 2 objects
D/LeakCanary: │    ↓ SnapshotMutableStateImpl$StateStateRecord.value
D/LeakCanary: │                                                ~~~~~
D/LeakCanary: ├─ androidx.compose.material.ripple.RippleHostView instance
D/LeakCanary: │    Leaking: YES (View.mContext references a destroyed activity)
D/LeakCanary: │    Retaining 3.8 kB in 31 objects
D/LeakCanary: │    View not part of a window view hierarchy
D/LeakCanary: │    View.mAttachInfo is null (view detached)
D/LeakCanary: │    View.mWindowAttachCount = 1
D/LeakCanary: │    mContext instance of xxx.xxxx.xxxxx.quizes.ESQuizActivity with mDestroyed = true
D/LeakCanary: │    ↓ View.mContext
D/LeakCanary: ╰→ xxx.xxxx.xxxxx.quizes.ESQuizActivity instance
D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because xxx.xxxx.xxxxx.quizes.ESQuizActivity received
D/LeakCanary: ​     Activity#onDestroy() callback and Activity#mDestroyed is true)
D/LeakCanary: ​     Retaining 2.6 MB in 9110 objects
D/LeakCanary: ​     key = 1cd7592e-be05-459a-bec0-664756a86aac
D/LeakCanary: ​     watchDurationMillis = 61894
D/LeakCanary: ​     retainedDurationMillis = 56869
D/LeakCanary: ​     mApplication instance of xxx.xxxx.xxxxx.general.Application
D/LeakCanary: ​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
D/LeakCanary: 
D/LeakCanary: 173531 bytes retained by leaking objects
D/LeakCanary: Signature: d96e2a7ef94b93420bf533642d3bfbd136d1ddf1
D/LeakCanary: ┬───
D/LeakCanary: │ GC Root: Thread object
D/LeakCanary: │
D/LeakCanary: ├─ android.os.HandlerThread instance
D/LeakCanary: │    Leaking: NO (PathClassLoader↓ is not leaking)
D/LeakCanary: │    Thread name: 'LeakCanary-Heap-Dump'
D/LeakCanary: │    ↓ Thread.contextClassLoader
D/LeakCanary: ├─ dalvik.system.PathClassLoader instance
D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking and A ClassLoader is never leaking)
D/LeakCanary: │    ↓ ClassLoader.runtimeInternalObjects
D/LeakCanary: ├─ java.lang.Object[] array
D/LeakCanary: │    Leaking: NO (SnapshotKt↓ is not leaking)
D/LeakCanary: │    ↓ Object[3439]
D/LeakCanary: ├─ androidx.compose.runtime.snapshots.SnapshotKt class
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking and a class is never leaking)
D/LeakCanary: │    ↓ static SnapshotKt.applyObservers
D/LeakCanary: ├─ java.util.ArrayList instance
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
D/LeakCanary: │    ↓ ArrayList[0]
D/LeakCanary: ├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
D/LeakCanary: │    Leaking: NO (Recomposer↓ is not leaking)
D/LeakCanary: │    Anonymous subclass of kotlin.jvm.internal.Lambda
D/LeakCanary: │    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
D/LeakCanary: ├─ androidx.compose.runtime.Recomposer instance
D/LeakCanary: │    Leaking: NO (Recomposer is in state PendingWork)
D/LeakCanary: │    ↓ Recomposer.snapshotInvalidations
D/LeakCanary: │                 ~~~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ java.util.ArrayList instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 5.8 MB in 32399 objects
D/LeakCanary: │    ↓ ArrayList[53]
D/LeakCanary: │               ~~~~
D/LeakCanary: ├─ java.util.HashSet instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 152 B in 5 objects
D/LeakCanary: │    ↓ HashSet[element()]
D/LeakCanary: │             ~~~~~~~~~~~
D/LeakCanary: ├─ androidx.compose.runtime.ParcelableSnapshotMutableState instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 72 B in 4 objects
D/LeakCanary: │    ↓ SnapshotMutableStateImpl.next
D/LeakCanary: │                               ~~~~
D/LeakCanary: ├─ androidx.compose.runtime.SnapshotMutableStateImpl$StateStateRecord instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 56 B in 3 objects
D/LeakCanary: │    ↓ SnapshotMutableStateImpl$StateStateRecord.value
D/LeakCanary: │                                                ~~~~~
D/LeakCanary: ├─ androidx.compose.ui.platform.AndroidComposeView$ViewTreeOwners instance
D/LeakCanary: │    Leaking: UNKNOWN
D/LeakCanary: │    Retaining 16 B in 1 objects
D/LeakCanary: │    lifecycleOwner instance of xxx.xxxx.xxxxx.elements.SentenceInfoActivity with mDestroyed = true
D/LeakCanary: │    savedStateRegistryOwner instance of xxx.xxxx.xxxxx.elements.SentenceInfoActivity with mDestroyed = true
D/LeakCanary: │    ↓ AndroidComposeView$ViewTreeOwners.lifecycleOwner
D/LeakCanary: │                                        ~~~~~~~~~~~~~~
D/LeakCanary: ╰→ xxx.xxxx.xxxxx.elements.SentenceInfoActivity instance
D/LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because xxx.xxxx.xxxxx.elements.SentenceInfoActivity received
D/LeakCanary: ​     Activity#onDestroy() callback and Activity#mDestroyed is true)
D/LeakCanary: ​     Retaining 173.5 kB in 2545 objects
D/LeakCanary: ​     key = aced267f-43ea-42e2-93ec-e44d84effa38
D/LeakCanary: ​     watchDurationMillis = 5685
D/LeakCanary: ​     retainedDurationMillis = 685
D/LeakCanary: ​     mApplication instance of xxx.xxxx.xxxxx.general.Application
D/LeakCanary: ​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
D/LeakCanary: ====================================
D/LeakCanary: 0 LIBRARY LEAKS
D/LeakCanary: 
D/LeakCanary: A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
D/LeakCanary: See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
D/LeakCanary: ====================================
D/LeakCanary: 0 UNREACHABLE OBJECTS
D/LeakCanary: 
D/LeakCanary: An unreachable object is still in memory but LeakCanary could not find a strong reference path
D/LeakCanary: from GC roots.
D/LeakCanary: ====================================
D/LeakCanary: METADATA
D/LeakCanary: 
D/LeakCanary: Please include this in bug reports and Stack Overflow questions.
D/LeakCanary: 
D/LeakCanary: Build.VERSION.SDK_INT: 28
D/LeakCanary: Build.MANUFACTURER: HUAWEI
D/LeakCanary: LeakCanary version: 2.8.1
D/LeakCanary: App process name: xxx.xxxx.xxxxx.D
D/LeakCanary: Stats: LruCache[maxSize=3000,hits=57955,misses=158500,hitRate=26%]
D/LeakCanary: RandomAccess[bytes=7962300,reads=158500,travel=87741508104,range=57423047,size=63349830]
D/LeakCanary: Heap dump reason: 6 retained objects, app is visible
D/LeakCanary: Analysis duration: 10397 ms
D/LeakCanary: Heap dump file path: /storage/emulated/0/Download/leakcanary-xxx.xxxx.xxxxx.D/2022-03-24_10-42-05_984.hprof
D/LeakCanary: Heap dump timestamp: 1648114943119
D/LeakCanary: Heap dump duration: 4425 ms
D/LeakCanary: ====================================

Update 2: I attached composables to a view obtained from databinding in an activity, like

binding.list.setContent { ... }
binding.list.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

I set the dispose strategy as I suspect the warning messages are about the fact that I didn't clear the view or destroy the composable on destroy of the activity. Not sure if it does what I thought. Then I also did

binding.list.removeAllViews()

in onDestroy, but these don't help.

0 Answers
Related