关于android:由浅入深聊聊-LeakCanary-的那些事

37次阅读

共计 8944 个字符,预计需要花费 23 分钟才能阅读完成。

引言

对于内存透露,Android 开发的小伙伴应该都再相熟不过了,比方最常见的动态类间接持有了某个 Activity 对象,又比方某个组件库的订阅在页面销毁时没有及时清理等等,这些状况下少数时都会造成内存透露,从而对咱们 App 的 晦涩度 造成影响,更有甚者造成了 OOM 的状况。

在现代化开发以及多人合作的背景下,如何能做到开发中疾速的监测内存透露,从而尽可能杜绝上述问题,此时就显得更加尤为重要。

LeakCanary 就是一个能够帮忙开发者疾速排查上述问题的工具,简直所有的 Android 开发者都曾应用过这个工具,其背地的设计也是各厂自研相应组件的借鉴思维。

而了解 LeakCanary 背地的设计思维与原理,也更是每个应用层开发者所必不可少的技能点。

故此,本篇将以最新的视角,与你一起使劲一瞥 LeakCanary

LeakCanary 版本:2.10

本篇定位 中等,将从背景到应用形式,再到源码解析,尽可能全面、易懂。

根底概念

在开始之前,咱们还是要解释一些常见的根底问题,以便更好的了解本篇。🤔

什么是内存透露?

当咱们 App 无奈开释不须要的对象援用时,即为内存透露。也能够了解为,生命周期长的持有了生命周期短的对象所导致

常见内存透露场景?

  • 非动态外部类与匿名外部类 (导致的持有外部类援用时, 比方Act 中的 非动态 Handler);
  • 异步线程持有内部 context(非 AppContext)援用所导致的内存透露;
  • service 遗记理解绑或者播送没有解除订阅等;
  • stream 流遗记敞开;

LeakCanary 应用形式

对于 LeakCanary 的应用形式,老手小伙伴能够从 官网文档 失去更多,这里仅仅只是作为一个简略概要。

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'

LeakCanary 应用很简略,只须要在 gradle 中增加依赖即可,就是这么 Easy :)

而后当咱们 app 运行后,桌面会装置一个 名为 Leask 的软件,icon 是一个小鸟的图标。

如果 app 在应用中呈现内存透露并且达到肯定数量时,其会自动弹出一个告诉,提醒咱们进行内存透露剖析。当点击告诉后,LeakCanary 会进行透露堆栈剖析,并将其显示到 Leask 的透露列表中。开发者能够通过具体的 item 从而理解相应的透露信息,当然也通过查看 log 日志进行剖析。具体如下图所示(官网截图):

源码剖析

这一章节,咱们将从 LeakCanary 的源码登程,从而摸索其背地的设计思维。

如何初始化

问起这个问题,稍有教训的开发者必定都会猜到,既然不须要手动初始化,那必定是 ContentProvider 了。😉

如下所示:

internal class MainProcessAppWatcherInstaller : ContentProvider() {override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AppWatcher.manualInstall(application)
    return true
  }
 }

其外部减少一个 ContentPrvider , 并在 onCreate() 进行初始化。

不过 LeakCanary 也提供了 JetPack-startup 的形式, 如下所示:

在下面咱们能看到,上述的初始化时会调用 AppWatcher.manualInstall(application) 办法,而咱们的插入点也即从这里开始 📌

manualInstall(application)

顾名思义,用于进行初始化组件的装置。

上述的逻辑中,会先通过反射去给 AppWatcher.objectWatcher 进行赋值,而后装置具体的组件观察者,具体的源码剖析如下所示。


appDefaultWatchers()

创立默认组件观察者列表。

用于初始化咱们具体的观察者列表,目前是反对 ActivityFragmentViewService,并且这些观察者都传入了 一个动态的 ReachabilityWatcher 对象 objectWatcher

ReachabilityWatcher 是干什么的呢?

中文翻译过去时 可达性观察者

简略了解就是 用于监听咱们的对象是否将要立即变为弱可达,其自身只是一个接口,具体实现类为 ObjectWatcher,也即咱们上述初始化插件时传递的对象。

这里可能不是很好了解,对于具体的逻辑,咱们上面还会再进行解释,临时先有个印象即可。😶‍🌫️


loadLeakCanary(application)

val loadLeakCanary by lazy {
  try {val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
    leakCanaryListener.getDeclaredField("INSTANCE")
      .get(null) as (Application) -> Unit
  } catch (ignored: Throwable) {NoLeakCanary}
}

这里是用于初始化 InternalLeakCanary,不过因为 InternalLeakCanary 属于下层模块,无奈间接调用到,所以应用了 [反射] 去创立。

对于 sdk 开发者而言,这也是一个小技巧,应用反射的形式进行初始化,从而防止模块间的耦合。

InternalLeakCanary 相当于 LeakCanary 的外部具体实现,即也就是在这里进行具体的初始化工作。

咱们间接去看其源码即可:

上述源码次要做了一些初始化的工作,具体的内容,咱们在源码中减少了正文,具体不用过于深追。

不过对于 sdk 初始化局部,还是有值得咱们学习的一个小中央,这里独自提出来:

如上所示,这是用于监听 App 是否处于前台,相比一般的应用 Act 全局监听,这里还是用了播送,并监听了 ACTION_SCREEN_ON(屏幕唤醒并正在交互) 与 ACTION_SCREEN_OFF(屏幕敞开),从而实现了更加谨严的判断逻辑,值得咱们业务中参考。👏

LeakCanary 初始化局部到这里就完结了,相干的细节逻辑在下面都有形容,这里咱们就不再做叙述。

如何检测内存透露

在本大节,咱们将聊聊 LeakCanary 是如何做到监听 ActFragment 等内存透露,即具体的实现逻辑是怎么的,从而了解其设计的思维。

本大节不会波及具体的对象是否透露的判断,所以更多的是框架的封装思考。

在下面的初始化的源码剖析中,咱们能够发现,其最终会去调用下述办法去执行各组件的监听:

  • ActivityWatcher(application, reachabilityWatcher);
  • FragmentAndViewModelWatcher(application, reachabilityWatcher);
  • RootViewWatcher(reachabilityWatcher);
  • ServiceWatcher(reachabilityWatcher);

所以咱们本节的插入点就从这里开始🔺。

ActivityWatcher

用于监听 Activity 的观察者,具体实现如下所示:

如上述逻辑所示:外部注册了一个 Activity 的全局生命周期监听,从而在 onDestory() 时将 activity 的援用交给 ReachabilityWatcher 去解决判断。


FragmentAndViewModelWatcher

用于监听 FragmentViewModel 的观察者,具体源码如下:

上述逻辑中,咱们能够发现,对于 Fragment 的可达性监听计划,其和 Act 一样,先注册 Act-Lifecycle 监听,而后在 onCreate() 时进行 Fragment-Lifecycle 注册监听,外部调用了 FragmentManager 进行生命周期监听注册。

🔺 但因为咱们的 FragmentManager 实际上是有三个版本:

  • android.app.FragmentManager (Deprecated);
  • android.support.v4.app.FragmentManager;
  • androidx.fragment.app.FragmentManager;

上述版本,经验过的开发同学相必都很分明,过往的教训,这里就不多提了。一句话,都是泪 👾。

碍于一些历史起因,所以要针对三个版本都做一些判断解决。上述逻辑中,因为 app.FragmentManager 绑定生命周期时有限度,必须 8.0 之后才能够进行绑定,后两者则是别离判断了 AndroidXSupport

咱们这里轻易拎一个具体的解决代码,以 AndroidX 为例

如上所示,别离在 onFragmentViewDestroyed()onFragmentDestroyed()view 对象fragment 对象 进行了可达性追踪。

须要留神的是,在 invoke()onFragmentCreated() 办法中,外部还对 ViewModel 进行了可达性追踪,这也是反对追踪 ViewModel 内存透露的逻辑所在

相应的,咱们在看一眼 ViewModel 中具体的实现思路。


ViewModelClearedWatcher

用于监听 ViewModel 是否革除的观察者,具体源码如下:

在初始化时,会调用 install() 插入一个 ViewModel,这个 ViewModel 相似一个 [特务] 的作用,目标是在 ViewModel 销毁 时,即 onCleard() 办法执行时,通过反射拿到 ViewModelStore 中保留的 ViewModel 数组,从而去对每个 ViewModel 对象进行可达性追踪,从而判断是否存在内存透露。

联合在 Fragment 中的逻辑,所以残缺的流程大抵如下:


RootViewWatcher

用于监听 根视图 对象是否透露的观察者,具体源码如下:

初始化时创立了一个 OnRootViewAddedListener,用于拦挡所有根视图的创立,具体应用了 curtains 库实现。

以后窗口类型 是 DialogTooltipToast 或者 未知类型 时增加 View.OnAttachStateChangeListener 监听器,并初始化了一个 runable 用于执行 view 对象可达性追踪的回调,从而当这个 View 增加到窗口时,从 Handler 中移除该回调;在窗口移除时再增加到 Handler 中,从而触发 view 对象的可达性追踪。


ServiceWatcher

用于监听 服务 对象是否透露的观察者,具体源码如下:

上述的流程相对来说比较复杂,源码局部咱们做了大量删减,具体逻辑如下:

  • ServiceWatcherinstall() 时,会通过反射的形式取出 ActivityThread 中的 mH(Handler),并应用自定义的 CallBack 替换 Handler 中原来的 mCallBack,并缓存原来的 mCallBack,从而做到监听 service 的进行,并且连续原 callBack 流程的持续。当 Handler 中收到的音讯是 msg.what == STOP_SERVICE 时,则证实以后 service 行将进行,则将该 service 退出要追踪的服务汇合中。
  • 接下来 hook ActivityManagerService,并应用动静代理的形式去代理该 IActivityManager 对象,从而监听该对象的办法调用。如果以后调用的办法是 serviceDoneExecuting(),则证实 service 已真正完结。即从以后待追踪的服务汇合中取出该 service 并对其进行可达性追踪,并从该汇合中移除该 service 对象。

如何断定内存透露

本大节将要来到咱们的重头戏,即如何判断一个对象是否真的内存透露🧐。

在上述剖析中,咱们不难发现,对于对象的可达性追踪,即是否内存透露,最终都是调用了该办法:

reachabilityWatcher.expectWeaklyReachable(view,xxx)

reachabilityWatcher 只有一个具体的实现类,即 ObjectWatcher,所以咱们的插入点从这里开始🔺 ->

咱们去看看相应的 expectWeaklyReachable 源码,如下所示:

ObjectWatcher.expectWeaklyReachable()

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  ...
  // 因为所有追踪的对象默认都认为是行将被销毁,即弱可达对象。// 这里这里再次对 ReferenceQueue 呈现的弱援用进行移除
  removeWeaklyReachableObjects()
  // 生成一个随机的 UUID
  val key = UUID.randomUUID().toString()
  // 记录以后的监测开始工夫
  val watchUptimeMillis = clock.uptimeMillis()
  // 应用一个弱援用持有以后要追踪的 弱可达对象
  // 并且调用了基类的 WeakReference<Any>(referent, referenceQueue)结构器
  // 这样的话,弱援用在被回收之前会呈现到 referenceQueue 中
  val reference =
    KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
  // 将该援用对象存入察看 Map 中
  watchedObjects[key] = reference
  
  // 提早检测以后弱援用对象,从而判断对象是否被回收,如果没有,则证实可能存在内存透露
  // 默认提早 5s 后执行,具体参见上述 manualInstall()
  // this.retainedDelayMillis = TimeUnit.SECONDS.toMillis(5)
  checkRetainedExecutor.execute {moveToRetained(key)
  }
}

  @Synchronized private fun moveToRetained(key: String) {
    // 先将援用队列中的对象从队列中删除
    removeWeaklyReachableObjects()
    // 获取指定 key 对应的弱援用对象
    val retainedRef = watchedObjects[key]
    // 如果以后的弱援用对象不为 null, 则证实可能产生了内存透露
    if (retainedRef != null) {
      // 记录内存透露工夫,并告诉所有对象,以后已产生内存透露
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach {it.onObjectRetained() }
    }
  }

  private fun removeWeaklyReachableObjects() {
    // 将援用队列中的对象从队列中删除
    var ref: KeyedWeakReference?
    do {
      // 如果不为 null,则证实该对象曾经被回收
      // 
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }

上述办法中,先调用 removeWeaklyReachableObjects() 办法 对以后的援用队列进行了革除 。而后生成了 KeyedWeakReference 弱援用对象,外部持有者以后要追踪的对象,并且记录了以后的工夫,key 等信息。须要留神的是,这里在初始化 KeyedWeakReference 时,构造函数中还传入了 queue,而这样的目标是为了 再进行一遍对象是否回收的 check。而后将创立好的弱援用察看对象增加到咱们的察看 Map 中,并应用 Handler 提早 5s 后再去检测该对象是否真的被回收。

初始化 KeyedWeakReference,为什么要传入队列 queue?

当咱们弱援用中所持有的对象被回收时,即相当于咱们弱援用自身也没有用了,此时,java 会将咱们以后的弱援用对象,增加到咱们所传递的队列 (queue) 中去。即咱们能够通过某些逻辑去判断队列是否存在咱们指定的弱援用对象,如果存在,则证实对象曾经被回收,否则即存在透露的危险。

当 5s 提早完结后,调用 moveToRetained() 办法再次去检测该对象。检测时,仍然先调用 removeWeaklyReachableObjects() 将可能曾经被回收的对象进行革除,防止误判。此时如果以后咱们要检测的 key 所对应弱援用对象仍然存在,则证实该对象没有被失常回收,可能产生了内存透露。此时记录内存透露的产生的工夫,并告诉所有对象

所以接下来咱们去看看 onObjectRetained() 办法即可。

onObjectRetained()

InternalLeakCanary.onObjectRetained()

用于检测对象是否真的存在泄露,具体源码如下:

上述逻辑如下,先判断以后是否正在查看对象是否透露中,如果正在查看,则间接跳过,否则取得以后零碎工夫 + 须要提早的工夫(这里是 0s),并在后盾线程提早指定工夫后,再去检测是否透露。


checkRetainedObjects()

再次去查看以后仍未回收的对象,如果这次仍然存在,则证实真的透露了,这里相当于是最终审判。

上述逻辑如下,咱们分为三步来看:

  1. 外部会先调用 objectWatcher.retainedObjectCount 取得以后曾经透露的对象个数;

    如果你还记得咱们下面 提早 5s 再去检测对象是否透露的 moveToRetained() 办法,就会记得,该办法外部对 retainedUptimeMillis 字段进行了设置。

  2. 如果透露的数量 >0,则 GC 一次后再次获取透露个数;

    这里的 gcTrigger.runGc() 实则是调用 GcTrigger.Default.runGc()

    在零碎的正文中,应用 Runtime.getRuntime().gc() 能够比 System.gc() 更容易触发;(因为 java 的垃圾回收更多只是告诉执行,至于是否真的执行,实则是不确定的)。

    须要留神是,该办法外部在 GC 后还提早了 100ms , 从而以便使得虚拟机真的 GC 后,从而将弱援用挪动到咱们传递援用队列中去。(因为咱们在初始化 KeyedWeakReference 时,外部传递了一个援用队列), 这里依然在保底 check。

  3. 接着再次调用 checkRetainedCount() 判断以后透露的对象是否达到阈值,如果达到了,则间接 dump heap , 并收回一个内存透露的告诉,否则则只打印一下透露的日志。

总结

在本篇中,咱们通过对于 LeakCanary 的应用形式以及应用层的实现原理做了较完整的剖析,从而以一个直观的视角了解其应用层的设计思维。最初让咱们咱们再次去回顾一下上述整个流程:

  1. 初始化做了什么?

    因为 LeakCanary 应用了 ContentProvider,所以初始化的逻辑不须要开发者手动染指,默认在初始化的外部,其会注册 App 全局的生命周期监听,并且初始化了相应的监听插件,比方 对于 Activity 的 ActivityWatcher,Fragment 和 ViewModel 的 FragmentAndViewModelWatcher 等。

  2. 各组件的内存透露监听计划是怎么设计的呢?

    • Activity(ActivityWatcher)

      外部注册了一个 Activity 的全局生命周期监听,从而在 onDestory() 时去追踪以后 activity 对象是否内存透露。

    • Fragment(FragmentAndViewModelWatcher)

      先注册 Act-Lifecycle 监听,而后在 onCreate() 时进行 Fragment-Lifecycle 注册监听,并在 onFragmentViewDestroyed()onFragmentDestroyed() 对 view 对象 与 fragment 对象 进行了内存透露追踪。

    • RootViewWatcher(RootViewWatcher)

      应用 curtains 库监听所有根 View 的创立与销毁,并初始化了一个 runable 用于监听视图是否透露。在以后 view 被增加到窗口时,则从 handler 中移除该 handler;如果以后 view 从窗口移除时,则触发该 runable 的执行。

    • 其余组件可在具体的源码剖析开端,查看总结即可,这里就不再复述了😉
  3. 如何断定内存透露呢?

    对于要监听的对象,应用 KeyedWeakReference 与其进行关联(初始化时传入了一个援用队列 queue),并将其保留到专门的 察看 Map 中。这样当该对象被 Gc 回收时,就会呈现在 相应的援用队列中。而后,在主线程提早 5s 后去判断是否存在内存透露。

    在具体的判断逻辑中,会先将援用队列中呈现的对象从要察看的 Map 中移除,从而防止误判。而后再判断以后要察看的对象是否存在,如果不存在,则阐明没有内存透露;否则意味着可能呈现了内存透露,则调用 Runtme.getRunTime().gc() 进行 GC 告诉,并且期待 100ms 后再次执行判断,若该察看的对象依然存在于 观察者 Map 中,则证实该对象真的曾经透露,此时就会依据内存透露的个数 弹出告诉 或者开始 dump hprof

    至此,对于 LeakCanary 的应用层剖析,到这里就完结了。

    更深层的如何生成 hprof 文件 以及其解析形式,这并非本篇所要摸索的方向,当然如果你也比拟感兴趣,能够通过查阅其他同学的材料从而失去更加深刻的了解🧐。

参阅

  • LearkCanary 文档
  • Yorkek’s – LeakCanary2 源码解析

对于我

我是 Petterp , 一个 Android 工程师,如果本文对你有所帮忙,欢送 点赞、评论、珍藏,你的反对是我继续创作的最大激励!

正文完
 0