聊一聊LeakCanary的RootViewWatcher如何实现全局Root View监听

3,666 阅读4分钟

一. 前言

之前有些过两篇分析LeakCanary如何监听各种对象的文章:

LeakCanary如何监听Fragment、Fragment View、ViewModel销毁时机?

LeakCanary如何监听Service、Root View销毁时机?

其实,在分析LeakCanary如何监听Root View(如Dialog、Toast窗口根View)的时候,并没有分析RootViewWatcher是怎么实现监听应用所有窗口根View的添加和移除的(当时我也不懂),只有下面孤零零的一段牛逼的监听代码:

override fun install() {
    Curtains.onRootViewsChangedListeners += listener
}

override fun uninstall() {
    Curtains.onRootViewsChangedListeners -= listener
}

这也算是留下了一个遗憾吧,毕竟学习到如何监听应用所有窗口Root View的添加和移除的这种技巧,不管是对源码的认知,还是对以后的项目功能的启发都一定会有帮助的。

现在就来解开谜底了,Curtains是大名鼎鼎的Square提供的一个window帮助库,而RootViewWatcher就是借助这个库监听Root View添加和移除,所以我们就带着上面的问题细细分析下这个库的实现机制了。

PS:分析的Curtains库依赖版本:

dependencies {
    implementation 'com.squareup.curtains:curtains:1.2.4'
}

二. Curtains探究

Curtains.onRootViewsChangedListeners瞧一瞧

这里我们直接以上面的Curtains如何帮助RootViewWatcher监听Root View添加作为切入点分析:

继续往下看:

其中rootViewsSpyRootViewsSpy类的实例对象,同时还是Curtains内的一个属性。

可以看到,最终会将listener添加到RootViewsSpylisteners集合中。

这里顺便说个知识点,+=是集合定义的运算符重载扩展函数实现的,相当于实现了add()操作。

接下来的目标就要寻找这个listeners集合中的元素什么时候被取出并执行的,接下来,我们先看下创建RootViewsSpy对象时发生了哪些操作


RootViewsSpy的创建初始化

上面有说,rootViewsSpyCurtains的一个属性,并且还是通过懒加载创建的,懒加载代码块中调用RootViewsSpy.install()完成最终的创建,我们看下这个方法:

该方法创建RootViewSpy同时,还调用了非常关键的方法WindowManagerSpy.swapWindowManagerGlobalMViews{}

这个方法从表面上看,就是将该方法内部的mViews添加到delegatingViewList中,其中这个mViews就是收集的应用全局所有的Root View对象,至于怎么获取的我们下面会进行分析的,这里我们看下delegatingViewList的操作:

delegatingViewList重写了集合的add()方法,这个方法会遍历上面的RootViewsSpy.listeners对象,最终执行了我们添加的listener对象,从而最终实现了对于Root Viewattachdetach的内存泄漏监听。

所以,WindowManagerSpy.swapWindowManagerGlobalMViews{}就是我们本篇文章讲解的核心,接下来我们看下它是如何收集应用所有的Root View的。


WindowManagerSpy.swapWindowManagerGlobalMViews{}探究

关键代码mViewsField[windowManagerInstance] as ArrayList<View>,通过这个最终拿到了应用所有的Root View,并且很明显是通过反射获取的,其中mViewsField属性对应windowManagerInstance的一个字段。

接下来我们看下最关键的mViewsField是个啥。


mViewsField是个啥?

mViewsField这个代表这windowManagerClass类中一个叫mViews的字段,windowManagerClass这个是个啥类:

到这里,终于破案了,windowManagerClass就是WindowManagerGlobal对象,这个对象相信对于View渲染流程比较熟悉的人应该不会默认,这里我们简单说下这个类的作用:

界面渲染流程是从ActivityonResume开始的,其中在onResume界面会调用WindowManager.addView()方法完成窗口的添加,而这个方法经过WindowManagerImpl(WindowManager是接口,这个是其实现类)最终会调用到WindowManagerGlobal.addView(View)方法;

在这个方法中会将界面要渲染显示的根View添加到WindowManagerGlobal.mViews对应的集合中,所以只要我们能拿到WindowManagerGlobal.mViews ,自然就可以拿到应用所有的Root View

像常见的DialogToast等显示的界面最终也是会调用WindowManager.addView()方法走到上面的这个流程中的。

其中,WindowManagerGlobal对象实例可以通过其提供的getInstance()静态方法获取:

对应在WindowManagerSpy中获取该实例对象的方法为:

private val windowManagerInstance by lazy(NONE) {
    windowManagerClass?.let { windowManagerClass ->
      val methodName = if (SDK_INT > 16) {
        "getInstance"
    } else {
        "getDefault"
    }
      windowManagerClass.getMethod(methodName).invoke(null)
  }
}

三. 总结

从上面的流程中,我们可以得到这个机制实现的一个大致流程:

  1. 由于所有窗口的添加都会经过WindowManagerGlobal.addView(View)的方法,所以自然可以拿到窗口的根View并保存到WindowManagerGlobal.mViews中,所以我们通过反射获取mViews字段实例;

  2. 接着我们自定义一个ArrayList,监听集合的add()removeAt()方法,并通过反射将其赋值给mViews

  3. 最终通过监听的add()方法调用外部传入的OnRootViewsChangedListener集合实例,也就是我们文章一开始提到的listener,最终完成了对于Root Viewattachdetach监听:

四. 后续

其实上面的这个用法只是curtains提供的一个小功能而已,对于这个curtains的其他功能探究后续有空会再一一进行分析。


本文正在参加「金石计划」