肢解LeakCanary:精通BackgroundTrigger的“狠活”

1,237 阅读4分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

本篇文章主要是分析LeakCanary一个不起眼的类:BackgroundTrigger。当出现内存泄漏时,这个类可以帮助我们当应用处于后台状态时再执行相关的dump heap操作。

依赖如下:

implementation 'com.squareup.leakcanary:leakcanary-android-release:2.9.1'

还没分析这个类之前相信大家下意识的通过下面两种方式实现前后台监听:

  1. 通过Application.registerActivityLifecycleCallbacks()添加回调的方式来分析当前应用处于前台还是后台

  2. 使用Application级别的ProcessLifecycleOwner添加Observer的方式监听前后台

但是,BackgroundTrigger不是完全这么实现的,并且其监听应用是否切换到后台状态和上面两种方式监听应用切换到后台状态,这两个后台状态一些情况不是等价的,比如:

上面两种监听到后台状态,而BackgroundTrigger不一定处于所监听的后台状态,而BackgroundTrigger监听到的后台状态,一定意味着上面两种方式监听也是处于后台状态。

除此之外,BackgroundTrigger还有一个其他的狠活非常值得大家学习一下,接下来我们开始分析。

监听应用是否处于后台

这里我们从BackgroundTrigger的入口方法start()分析:

fun start() {
  checkMainThread()
  backgroundListener.install(application)
}

先检测是否为主线程,不是会抛出异常,接下来调用backgroundListenerinstall()方法,我们先看下backgroundListener是个啥:

image.png

可以看到backgroundListener就是一个BackgroundListener类型,并且实现了ActivityLifecycleCallbacks接口:

image.png

接下来就要分析BackgroundListenerinstall()方法:

fun install(application: Application) {
  application.registerActivityLifecycleCallbacks(this)
  ...
  checkAppInBackground.run()
}

由于BackgroundListener实现了ActivityLifecycleCallbacks,所以这里通过传递过来的Application注入到监听应用所有Activity生命周期回调的一个回调集合中,这个大家狠熟悉了。

先前我说过BackgroundTrigger并不是完全通过registerActivityLifecycleCallbacks实现了,但是还是借助了其实现。

下面我们直接看下BackgroundTrigger实现重写的onActivityPaused()监听方法:

override fun onActivityPaused(activity: Activity) {
  mainHandler.removeCallbacks(checkAppInBackground)
  //BACKGROUND_DELAY_MS为1s
  mainHandler.postDelayed(checkAppInBackground, BACKGROUND_DELAY_MS)
}

通过Hanlder延迟1s执行一个checkAppInBackground

private val checkAppInBackground: Runnable = object : Runnable {
  override fun run() {
    val appInBackgroundNow = processInfo.isImportanceBackground
    updateBackgroundState(appInBackgroundNow)
    if (!appInBackgroundNow) {
      mainHandler.removeCallbacks(this)
      mainHandler.postDelayed(this, BACKGROUND_REPEAT_DELAY_MS)
    }
  }
}

关键代码就是processInfo.isImportanceBackground,这个就是本文要讲如何监听应用是否已经切换到后台状态的:

override val isImportanceBackground: Boolean
  get() {
    ActivityManager.getMyMemoryState(memoryOutState)
    return memoryOutState.importance >= RunningAppProcessInfo.IMPORTANCE_BACKGROUND
  }
  1. 调用ActivityManagergetMyMemoryState()方法,memoryOutState是一个RunningAppProcessInfo类型:

    image.png

    通过注释可以看到获取当前这个应用进程的内存状态信息,并填充信息到当前传入的RunningAppProcessInfo对现象中,并指明了填充的哪些字段(没指明的不进行填充):

    image.png

  2. 然后使用被填充的RunningAppProcessInfoimportance判断,如果大于等于RunningAppProcessInfo.IMPORTANCE_BACKGROUND(400)就认为处于后台状态:

    这个400的状态代表着当前应用进程应用不再积极活跃的运行我们相关的组件了:

    image.png

    也就意味着并不是当我们home掉应用,就认为其处于后台状态,只有其处于一个非常不活跃的响应状态才认定其处于后台进程状态,这也就是开头说的那两种方式监听的后台状态和BackgroundTrigger监听到的后台状态不同的地方。

    后者这样实现的目的,就是为了尽可能减少dump heap对我们应用进程的影响。

我们回到checkAppInBackground,可以看到其执行isImportanceBackground后台状态检测后,会再次延迟5s再去通过postDelay()去调用自身检测是否处于后台状态。

动态代理优化接口方法的重写

之前有写过这样的一篇文章:接口使用额外重写的无关方法太多?优化它,就是为了解决,当我们实现某个接口时只想重写其中的一个必须的方法,而不是重写所有的方法(包括无关的)。

然后BackgroundTrigger来了一个狠活,通过动态代理的方式创建一个接口的实现类对象并委托给要实现这个接口的类BackgroundListener就是一个典型的例子:

image.png

上面说过,BackgroundListener实现了ActivityLifecycleCallbacks接口,这个接口要重写的方法可是非常多,但是我们看看BackgroundListener重写了几个方法:

image.png

就实现了其中的两个方法,其他的一个都没实现 ,关键就是BackgroundListener将要实现的ActivityLifecycleCallbacks接口委托给了noOpDelegate返回的实现类,我们看下这个方法:

internal inline fun <reified T : Any> noOpDelegate(): T = leakcanary.internal.noOpDelegate()

internal inline fun <reified T : Any> noOpDelegate(): T {
  val javaClass = T::class.java
  return Proxy.newProxyInstance(
    javaClass.classLoader, arrayOf(javaClass), NO_OP_HANDLER
  ) as T
}

private val NO_OP_HANDLER = InvocationHandler { _, _, _ ->
  // no op
}

这就是科技与狠话啊,真是万万没想到哈。利用内联函数的特性,借助于泛型实化,通过动态代理的方式创建一个ActivityLifecycleCallbacks的实现类。

总结

本篇文章主要是讲解了BackgroundTrigger如何监听后台状态的,相比较于传统的方式,这种监听后台状态的方式更加严格,进行dump heap时会大大减少对我们应用的影响,以及最后面的黑科技:动态代理的方式创建一个接口的实现类对象并委托给要实现这个接口的类,减少接口不需要的方法的重写。