【问题标题】:Can a not removed ViewTreeObserver listener cause memory leaks?未删除的 ViewTreeObserver 侦听器会导致内存泄漏吗?
【发布时间】:2025-01-13 15:35:01
【问题描述】:

我正在使用OnGlobalLayoutListener 监听视图的布局更改:

view.getViewTreeObserver().addOnGlobalLayoutListener(myListener);

由于我对此侦听器的事件感兴趣,只要视图存在,我认为无需调用 removeOnGlobalLayoutListener(myListener)

这会导致内存泄漏还是监听器垃圾会与视图一起收集?假设监听器持有对视图的引用。


背景是我想创建一个可以附加到某些视图并根据布局更改执行操作的模块。如果不需要移除,那么它的创建就像new FancyModule(theView) 一样简单,然后构造函数负责绑定侦听器。如果需要删除,我必须实现一个我想阻止的析构函数。

【问题讨论】:

    标签: android memory-leaks


    【解决方案1】:

    潜在的内存泄漏仅取决于您的架构。

    通常情况下,不拨打removeOnGlobalLayoutListener(myListener) 也没关系。 View 持有对 ViewTreeObserver 的引用,它持有对添加的 OnGlobalLayoutListener 的引用。如果您没有对侦听器的其他引用,则它是沿视图收集的垃圾。

    现在,如果您的 OnGlobalLayoutListener 实现包含对视图的引用,它仍然可以。引用循环对 Android 的垃圾收集器来说不是问题。

    如果您有另一个组件引用OnGlobalLayoutListener 实现,则可能会产生问题。如果组件的寿命比视图长(例如,它是通过应用程序对象保存的),那么您会通过侦听器创建视图(和上下文)的内存泄漏。

    当视图不再使用时,不要保留视图,这一点很重要。避免泄漏视图的一种简单方法是使用WeakReference

    【讨论】:

    • 监听器和视图只持有彼此的循环引用。不涉及其他组件。我不确定视图的 getViewTreeObserver() 是否可能返回比视图本身寿命更长的对象。
    • 好吧,如果没有其他东西从外部保存引用循环,那就没关系了。
    • 这是完全错误的 IMO。侦听器被添加到 ViewRoot 树观察者中,因此您的视图可能会被收集,但根视图可能不会。这很容易重现。
    • “我不确定视图的 getViewTreeObserver() 是否可能返回比视图本身寿命更长的对象。” -> 它确实@McFarlane
    【解决方案2】:

    是的,它可能会泄漏。这是来自 LeakCanary 的示例跟踪,

    • com.xxx.Activity 已泄露:
    • GC ROOT 静态 android.view.inputmethod.InputMethodManager.sInstance
    • 参考android.view.inputmethod.InputMethodManager.mCurRootView
    • 参考 com.android.internal.policy.DecorView.mAttachInfo
    • 参考android.view.View$AttachInfo.mTreeObserver
    • 参考 android.view.ViewTreeObserver.mOnGlobalLayoutListeners
    • 引用 android.view.ViewTreeObserver$CopyOnWriteArray.mData
    • 引用 java.util.ArrayList.elementData
    • 引用数组 java.lang.Object[].[0]
    • 引用 com.xxx.Activity$setExpandedToolbarHeight$layoutListener$1.this$0(android.view.ViewTreeObserver$OnGlobalLayoutListener 的匿名实现)
    • 泄露 com.xxx.Activity 实例

    【讨论】:

    • 你能用代码 sn-ps 扩展你的答案吗?显示你是如何使用监听器的?
    【解决方案3】:

    我有同样的内存泄漏问题,我尝试在片段的 onDestroyView 中注销 OnGlobalLayoutListener 但问题仍然存在,然后我尝试为我的视图添加 onDetachListener 然后注销 OnGlobalLayoutListener 并且它起作用了。

    在我使用的 kotlin 中:

    view?.doOnDetach {
        onGlobalLayoutListener?.let {
            view?.viewTreeObserver?.removeOnGlobalLayoutListener(it)
        }
        onGlobalLayoutListener = null
    }
    

    您也可以使用 addOnAttachStateChangeListener 方法。

    【讨论】:

    • 我尝试了其他解决方案,但都没有奏效(删除 onDestroy、onDestroyView、onDetach),最后必须将我的类从 Java 转换为 Kotlin 才能使用 doOnDetach,因为它是 Kotlin 扩展的一部分,终于解决了这个问题。谢谢!
    【解决方案4】:

    我遇到了同样的内存泄漏问题,即使我删除了 onDetachedFromWindow 中的 onGlobalLayoutListener,泄漏仍然发生:

    override fun onAttachedToWindow() {
        super.onAttachedToWindow() 
        view.viewTreeObserver.addOnGlobalLayoutListener(myListener); 
    }
    
    override fun onDetachedFromWindow() {  
        view.viewTreeObserver.removeOnGlobalLayoutListener(myListener); 
        super.onDetachedFromWindow() 
    }
    

    然后我在类似的问题上找到了answer

    由于您使用的是子视图的 ViewTreeObserver,因此行为稍微复杂一些,一种可能的解决方案是向您的 scrollView 添加一个 OnAttachStateChangeListener 并从那里添加/删除您的 OnScrollChangedListener。

    无论如何,关于泄漏的原因: getViewTreeObserver() 之后不会返回相同的实例 视图已与窗口分离。打电话 removeOnScrollChangedListener() 可能没有效果,保持你的 原来的 OnScrollChangedListener 仍然附加到旧的 ViewTreeObserver,因此泄漏了您的上下文。

    view 上使用OnAttachStateChangeListener 并按照建议删除侦听器并没有帮助,因此,解决方案是使用活动根视图的ViewTreeObserver

    private val activityRootView: View = (context as Activity).window.decorView.findViewById(android.R.id.content)
    
    override fun onAttachedToWindow() {
        super.onAttachedToWindow() 
        activityRootView.viewTreeObserver.addOnGlobalLayoutListener(myListener); 
    }
    
    override fun onDetachedFromWindow() { 
        activityRootView.viewTreeObserver.removeOnGlobalLayoutListener(myListener);
        super.onDetachedFromWindow() 
    }
    

    并且泄漏不再发生。

    【讨论】: