ComposeView leak: ViewTreeObserver.OnGlobalLayoutListener leaking even if it is removed during onDispose

275 Views Asked by At

So I have this Composable that I use to detect if a keyboard is visible:

@Composable
fun keyboardVisibilityAsState(): State<Boolean> {
    val keyboardState = remember { mutableStateOf(false) }
    val view = LocalView.current
    DisposableEffect(view) {
        val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
            val rect = Rect()
            view.getWindowVisibleDisplayFrame(rect)
            val screenHeight = view.rootView.height
            val keypadHeight = screenHeight - rect.bottom
            keyboardState.value = keypadHeight > screenHeight * 0.15
        }
        view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)

        onDispose {
            view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
        }
    }

    return keyboardState
}
...
// usage:
val isKeyboardVisible by keyboardVisibilityAsState()

It's being used inside another Composable, which in turn is used in a Fragment via ComposeView. Whenever I exit from the Fragment that uses this Composable, LeakCanary flags my Composable as the source of a leak, specifically:

┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│    Leaking: NO (InputMethodManager↓ is not leaking and a class is never
│    leaking)
│    ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│    Leaking: NO (InputMethodManager is a singleton)
│    ↓ InputMethodManager.mCurRootView
│                         ~~~~~~~~~~~~
├─ android.view.ViewRootImpl instance
│    Leaking: UNKNOWN
│    Retaining 16.6 kB in 405 objects
│    mContext instance of com.android.internal.policy.DecorContext, wrapping
│    activity com.someapp.ui.home.HomeActivity with mDestroyed
│    = false
│    ViewRootImpl#mView is not null
│    mWindowAttributes.mTitle = "com.someapp.uat/com.someapp.
│    someapp.ui.home.HomeActivity"
│    mWindowAttributes.type = 1
│    ↓ ViewRootImpl.mAttachInfo
│                   ~~~~~~~~~~~
├─ android.view.View$AttachInfo instance
│    Leaking: UNKNOWN
│    Retaining 678.8 kB in 11728 objects
│    ↓ View$AttachInfo.mTreeObserver
│                      ~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver instance
│    Leaking: UNKNOWN
│    Retaining 677.5 kB in 11691 objects
│    ↓ ViewTreeObserver.mOnGlobalLayoutListeners
│                       ~~~~~~~~~~~~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver$CopyOnWriteArray instance
│    Leaking: UNKNOWN
│    Retaining 677.2 kB in 11677 objects
│    ↓ ViewTreeObserver$CopyOnWriteArray.mData
│                                        ~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 677.2 kB in 11675 objects
│    ↓ ArrayList[0]
│               ~~~
├─ com.someapp.common.compose.utils.
│  KeyboardUtilsKt$keyboardVisibilityAsState$1$$ExternalSyntheticLambda0
│  instance
│    Leaking: UNKNOWN
│    Retaining 677.1 kB in 11673 objects
│    ↓ KeyboardUtilsKt$keyboardVisibilityAsState$1$$ExternalSyntheticLambda0.f$0
│                                                                            ~~~
├─ androidx.compose.ui.platform.AndroidComposeView instance
│    Leaking: UNKNOWN
│    Retaining 677.1 kB in 11669 objects
│    View not part of a window view hierarchy
│    View.mAttachInfo is null (view detached)
│    View.mWindowAttachCount = 1
│    mContext instance of dagger.hilt.android.internal.managers.
│    ViewComponentManager$FragmentContextWrapper, wrapping activity com.
│    someapp.ui.home.HomeActivity with mDestroyed = false
│    ↓ View.mParent
│           ~~~~~~~
╰→ androidx.compose.ui.platform.ComposeView instance
​     Leaking: YES (ObjectWatcher was watching this because com.
​     someapp.feature.featurea.ui.createpost.
​     CreatePostFragment received Fragment#onDestroyView() callback (references
​     to its views should be cleared to prevent leaks))
​     Retaining 1.6 kB in 29 objects
​     key = ccfc3149-9a0c-464a-928a-8329be9aa408
​     watchDurationMillis = 12212
​     retainedDurationMillis = 7209
​     View not part of a window view hierarchy
​     View.mAttachInfo is null (view detached)
​     View.mWindowAttachCount = 1
​     mContext instance of dagger.hilt.android.internal.managers.
​     ViewComponentManager$FragmentContextWrapper, wrapping activity com.
​     someapp.ui.home.HomeActivity with mDestroyed = false

METADATA

Build.VERSION.SDK_INT: 33
Build.MANUFACTURER: samsung
LeakCanary version: 2.12
App process name: com.someapp.uat
Class count: 35246
Instance count: 261168
Primitive array count: 171598
Object array count: 39671
Thread count: 85
Heap total bytes: 40863658
Bitmap count: 30
Bitmap total bytes: 21878570
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/com.someapp.uat/databases/com.google.android.
datatransport.events
Db 2: closed /data/user/0/com.someapp.
uat/databases/google_app_measurement_local.db
Db 3: open /data/user/0/com.someapp.uat/databases/someapp_db
Stats: LruCache[maxSize=3000,hits=133609,misses=253576,hitRate=34%]
RandomAccess[bytes=12798605,reads=253576,travel=112908047953,range=43409570,size
=57885308]
Analysis duration: 411616 ms

I was just wondering why I'm still getting this leak even if I specifically remove the listener during onDispose. Anyone experienced something similar?

0

There are 0 best solutions below