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.