View.getViewTreeObserver使用问题 & Safe封装

405 阅读5分钟

View.getViewTreeObserver使用问题 & Safe封装

最近排查一例内存泄漏,用例是直播间滑动切换300次,内存是有增长,但并非Activity泄漏。经分析hprof,发现是直播间内的一个容器Fragment实例一直在增长,发生泄漏。

image.png

image.png

最后定位到是某个业务类中使用 OnGlobalLayoutListener 引发泄漏了。业务代码示意如下:

class BottomToolbarComponent {

    fun showEmotionEntrance() {
        // getEmotionSvgaView 里可能返回mDynamicEmotionSvgaLandscape 或 mDynamicEmotionSvgaPortrait
         val viewTreeObserver = getEmotionSvgaView()?.viewTreeObserver
         MLog.debug(TAG, "showEmotionEntrance viewTreeObserver=\$viewTreeObserver")
         viewTreeObserver?.addOnGlobalLayoutListener(setEntrancePointObserver)
    }

    override fun onDestroyView() {
        val viewTreeObserver1 = mDynamicEmotionSvgaLandscape?.viewTreeObserver
        viewTreeObserver1?.removeOnGlobalLayoutListener(setEntrancePointObserver)
        val viewTreeObserver2 = mDynamicEmotionSvgaPortrait?.viewTreeObserver
        viewTreeObserver2?.removeOnGlobalLayoutListener(setEntrancePointObserver)
        MLog.debug(TAG, "onDestroyView viewTreeObserver1:\$viewTreeObserver1 viewTreeObserver2:\$viewTreeObserver2")
    }

}

初步看平平无奇,也是有对应释放的,但为啥泄漏了呢,加上日志后,就看到问题了:

add与remove时的observer对象不是同一个

这就导致了并没有真正的remove掉这个OnGlobalListener,它是一个闭包内部类,所以也就泄漏了外部类组件了。

原因

来看下View.getViewTreeObserver源码

public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
        return mAttachInfo.mTreeObserver;
    }

    if (mFloatingTreeObserver == null) {
        mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }

    return mFloatingTreeObserver;
}

可以看到,其返回的对象有两种可能:

  1. 如果View已经附加到了视图树(即mAttachInfo不为null),则直接返回mAttachInfo.mTreeObserver,这个ViewTreeObserver实例是与整个视图树关联的。
  2. 如果View还没有被添加到视图树中(即mAttachInfonull),则检查是否存在一个备用的ViewTreeObserver实例(mFloatingTreeObserver)。如果没有,则创建一个新的实例。这个备用的ViewTreeObserver是独立于视图树的,可以用于监听尚未添加到视图树中的View对象。

所以,不同时机去调用getViewTreeObserver api的话,有可能返回的对象是不同的。从而导致了注册和移除 相关listener不匹配,移除就失败了。

那该怎么使用?

我把首次getViewTreeObserver时返回的对象记录下来,之后再去调用其释放监听器不就好了么?

(此处停顿几秒)答案是还真不一定行:juejin.cn/post/691088… ,具体可见这篇文章。总结是:

当调用 _getViewTreeObserver_ 的时候,View 还未被加载到 Window 当中(_dispatchAttachedToWindow 未被调用_),此时提出变量,变量中存储的其实是用作临时存放 Observer 信息的 _mFloatingTreeObserver_,等到 View 加载到 Window 之后,_mFloatingTreeObserver 被 kill()_,再使用提出的变量的方法就会Crash。

关键就在于调用时机,所以直接使用官方的这个api,还有些麻烦,需要自己考虑上述的时机等情况。

时序场景分析

结合view 的attched 生命周期,和获取ViewTreeObserver 的时机,会有多种组合场景如下:

时序1:【当前view已Attached】,【View.getViewTreeObserver().addOnGlobalLayoutListener】,【View.getViewTreeObserver().removeOnGlobalLayoutListener()】【view detached】

  • 此case正常无问题,因为前后两次获取observer均是 当前view tree的observer对象,可以正确释放listener

时序2: 【View.getViewTreeObserver().addOnGlobalLayoutListener】,【当前view已Attached】,【View.getViewTreeObserver().removeOnGlobalLayoutListener()】【view detached】

  • 此case 并无问题。虽然前后两次获取到的observer是不同对象,add时是new 了一个floatingObserver,remove时则拿到了view tree observer。但当view attched 到window时,会将floatingObserver merge 到view tree observer上(其add的listener也),所以最终用view tree observer去移除listener也是正确了。

时序3: 【View.getViewTreeObserver().addOnGlobalLayoutListener】,【当前view已Attached】,【view detached】, 【View.getViewTreeObserver().removeOnGlobalLayoutListener()】

  • 此case 有问题,前后两次获取到的observer是不同对象,都是当时分别new的floatingObserver。这就导致了最后的removeListener其实没作用到view tree observer上,所以发生泄漏了。

时序4: 【当前view已Attached】,【View.getViewTreeObserver().addOnGlobalLayoutListener】,【view detached】, 【View.getViewTreeObserver().removeOnGlobalLayoutListener()】

  • 此case 有问题,前后两次获取到的observer是不同对象,add时是view tree observer,remove时则是new的一个新的floating observer。这就导致了最后的removeListener其实没作用到view tree observer上,所以发生泄漏了。开篇的泄漏bug单就是这种情况。

若采用记录下add时的observer对象,remove时也用同个对象的方案,看似解决了内存泄漏问题,但可能也有上文提到的崩溃问题,譬如这种时序下:

【View.getViewTreeObserver().addOnGlobalLayoutListener】,【当前view已Attached】,【view detached】, 【同个observer.removeOnGlobalLayoutListener()】

  • 此情况也有问题,一开始把add时获取到的observer记录下来(其实就是floating observer),期望在remove时去使用。但当view attahced到window后,这个floating observer其实就无效了(alive=false),所以之后去执行remove的话,则会抛出异常崩溃。

封装解决思路

从上述时序分析,可以看出问题关键在于 remove时的observer对象。

若remove时的observer对象是view tree observer,其实是可以正确释放listener,解决这类内存泄漏问题。

基于此理解,做了如下封装设计:

  1. 用一个单例map,记录 view 和 observer 的key-value对。期望remove和add时优先使用同一个observer,那么在时序1和4的场景下,均是用了view tree observer来操作,都是ok的。
  2. 若remove时发现当初记录的observer isAlive=false了,则说明该observer无效了,这时就尝试去找当前activity的decorView的viewTreeObserver,来执行remove操作,保证正确释放。可以带入时序2、3场景,发现也是ok的。
  3. 引入了单例map,本身也需要释放key,那么就在业务调用View.getViewTreeObserver().removeOnGlobalLayoutListener() 时,也同步移除map里的key,避免单例map持有view和observer的泄漏。

提供一份Safe封装

使用方式:View.safeViewTreeObserver()

对使用者来说,还是可以直接用。基本可以忽略调用时机,更省心了*。*

具体代码可见这里

欢迎一起交流讨论。