查看原文
其他

LeakCanary 新版 2.x ,你应该知道的知识点

RicardoMJiang 鸿洋 2021-10-12

本文作者


作者:RicardoMJiang

链接:

https://juejin.cn/post/6968084138125590541

本文由作者授权发布。


前言


LeakCanary是一个简单方便的内存泄漏检测框架,相信很多同学都用过,使用起来非常方便,它有以下几个特点:


1.不需要手动初始化。


2.可自动检测内存泄漏并通过通知报警。


3.不能用于线上。


那我们自然可以提出以下几个问题:


1.说一下LeakCanary检测内存泄漏的原理与基本流程。


2.LeakCanary是如何初始化的?


3.说一下LeakCanary是如何查找内存泄露的?


4.为什么LeakCanary不能用于线上?


本文主要梳理LeakCanary内存泄漏检测的主要流程并回答以上几个问题。


1LeakCanary检测内存泄漏的原理与基本流程


1.1 内存泄漏的原理


内存泄漏的原因:不再需要的对象依然被引用,导致对象被分配的内存无法被回收。


例如:一个Activity实例对象在调用了
onDestory方法后是不再被需要的,如果存储了一个引用Activity对象的静态域,将导致Activity无法被垃圾回收器回收。


引用链来自于垃圾回收器的可达性分析算法:当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。如图:


对象object5、object6、object7 虽然互相有关联,但是它们到 GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。


在Java语言中,可作为GC Roots的对象包括下面几种:


1、虚拟机栈(栈帧中的本地变量表)中引用的对象。


2、方法区中静态属性引用的对象。


3、方法区中常量引用的对象。


4、本地方法栈中JNI(即一般说的Native方法)引用的对象。


1.2 LeakCanary检测内存泄漏的基本流程


知道了内存泄漏的原理,我们可以推测到LeakCanary的基本流程大概是怎样的。


1.在页面关闭后触发检测(不再需要的对象)。


2.触发GC,然后获取仍然存在的对象,这些是可能泄漏的。


3.dump heap然后分析hprof文件,构建可能泄漏的对象与GCRoot间的引用链,如果存在则证明泄漏。


4.存储结果并使用通知提醒用户存在泄漏。


总体流程图如下所示:



1.ObjectWatcher 创建了一个KeyedWeakReference来监视对象。


2.稍后,在后台线程中,延时检查引用是否已被清除,如果没有则触发GC。


3.如果引用一直没有被清除,它会dumps the heap 到一个.hprof 文件中,然后将.hprof 文件存储到文件系统。


4.分析过程主要在HeapAnalyzerService中进行,Leakcanary2.0中使用Shark来解析hprof文件。


5.HeapAnalyzer 获取hprof中的所有KeyedWeakReference,并获取objectId。


6.HeapAnalyzer计算objectId到GC Root的最短强引用链路径来确定是否有泄漏,然后构建导致泄漏的引用链。


7.将分析结果存储在数据库中,并显示泄漏通知。


这里只做一个总体的介绍,具体流程可以阅读下文。


2LeakCanary是如何自动安装的?


LeakCanary的使用非常方便,只需要添加依赖便可以自动初始化,这是如何实现的呢?


我们看一下源码,其实主要是通过ContentProvider实现的。


internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */

  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */

  internal class LeakCanaryProcess : AppWatcherInstaller()

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

当我们启动App时,一般启动顺序为:Application->attachBaseContext =====>ContentProvider->onCreate =====>Application->onCreate


ContentProvider会在
Application.onCreate前初始化,这样就调用到了LeakCanary的初始化方法。


实现了免手动初始化。


2.1 跨进程初始化


注意,AppWatcherInstaller有两个子类,MainProcess与LeakCanaryProcess。


其中默认使用MainProcess,会在App进程初始化。


有时我们考虑到LeakCanary比较耗内存,需要在独立进程初始化。


使用leakcanary-android-process模块的时候,会在一个新的进程中去开启LeakCanary。


2.2 LeakCanary2.0手动初始化的方法


LeakCanary在检测内存泄漏时比较耗时,同时会打断App操作,在不需要检测时的体验并不太好。


所以虽然LeakCanary可以自动初始化,但我们有时其实还是需要手动初始化。


LeakCanary的自动初始化可以手动关闭。


 <?xml version="1.0" encoding="utf-8"?>
 <resources>
      <bool name="leak_canary_watcher_auto_install">false</bool>
 </resources>

1.然后在需要初始化的时候,调用AppWatcher.manualInstall即可。


2.是否开始dump与分析开头:
LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)


3.桌面图标开头:重写
R.bool.leak_canary_add_launcher_icon或者调用LeakCanary.showLeakDisplayActivityLauncherIcon(false)


2.3 小结


LeakCanary利用ContentProvier进行了初始化。


ContentProvier一般会在
Application.onCreate之前被加载,LeakCanary在其onCreate()方法中调用了AppWatcher.manualInstall进行初始化。


这种写法虽然方便,免去了初始化的步骤,但是可能会带来启动耗时的问题,用户不能控制初始化的时机,这也是谷歌推出StartUp的原因。


不过对于LeakCanary这个问题并不严重,因为它只在Debug阶段被依赖。


3
LeakCanary如何检测内存泄漏?


3.1 首先我们来看下初始化时做了什么?


当我们初始化时,调用了AppWatcher.manualInstall,下面来看看这个方法,都安装了什么东西。


@JvmOverloads
fun manualInstall(
  application: Application,
  retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5)
,
  watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
  ....
  watchersToInstall.forEach {
    it.install()
  }
}

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
)
: List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}

可以看出,初始化时即安装了一些Watcher,即在默认情况下,我们只会观察Activity,Fragment,RootView,Service这些对象是否泄漏。


如果需要观察其他对象,需要手动添加并处理。


3.2 LeakCanary如何触发检测?


如上文所述,在初始化时会安装一些Watcher,我们以ActivityWatcher为例。


class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

可以看到在Activity.onDestory时,就会触发检测内存泄漏。


3.3 LeakCanary如何检测可能泄漏的对象?


从上面可以看出,Activity关闭后会调用到ObjectWatcher.expectWeaklyReachable


@Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  )
 {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID()
      .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }

    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

private fun removeWeaklyReachableObjects() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }  

可以看出:


1.传入的观察对象都会被存储在watchedObjects中。


2.会为每个watchedObject生成一个KeyedWeakReference弱引用对象并与一个queue关联,当对象被回收时,该弱引用对象将进入queue当中。


3.在检测过程中,我们会调用多次
removeWeaklyReachableObjects,将已回收对象从watchedObjects中移除。


4.如果watchedObjects中没有移除对象,证明它没有被回收,那么就会调用
moveToRetained


3.4 LeakCanary触发堆快照,生成hprof文件


moveToRetained之后会调用到HeapDumpTrigger.checkRetainedInstances方法。


checkRetainedInstances() 方法是确定泄露的最后一个方法了。


这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump,分析 dump 文件,找到引用链。


private fun checkRetainedObjects() {
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      onRetainInstanceListener.onEvent(DumpHappenedRecently)
      ....
      return
    }

    dismissRetainedCountNotification()
    val visibility = if (applicationVisible) "visible" else "not visible"
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    ) 
}

  private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  )
 {
     ....
       heapDumper.dumpHeap()
       ....
     lastDisplayedRetainedObjectCount = 0
     lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
     objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
     HeapAnalyzerService.runAnalysis(
       context = application,
       heapDumpFile = heapDumpResult.file,
       heapDumpDurationMillis = heapDumpResult.durationMillis,
       heapDumpReason = reason
     )
 }
}

1.如果retainedObjectCount数量大于0,则进行一次GC,避免额外的Dump。


2.默认情况下,如果retainedReferenceCount<5,不会进行Dump,节省资源。


3.如果两次Dump之间时间少于60s,也会直接返回,避免频繁Dump。


4.调用
heapDumper.dumpHeap()进行真正的Dump操作。


5.Dump之后,要删除已经处理过了的引用。


6.调用
HeapAnalyzerService.runAnalysis对结果进行分析。


3.5 LeakCanary如何分析hprof文件


分析hprof文件的工作主要是在HeapAnalyzerService类中完成的。


关于Hprof文件的解析细节,就需要牵扯到Hprof二进制文件协议,通过阅读协议文档,hprof的二进制文件结构大概如下:



解析流程如下所示:




简要说下流程:


1.解析文件头信息,得到解析开始位置。


2.根据头信息创建Hprof文件对象。


3.构建内存索引。


4.使用hprof对象和索引构建Graph对象。


5.查找可能泄漏的对象与GCRoot间的引用链来判断是否存在泄漏(使用广度优先算法在Graph中查找)。


Leakcanary2.0较之前的版本最大变化是改由kotlin实现以及开源了自己实现的hprof解析的代码,总体的思路是根据hprof文件的二进制协议将文件的内容解析成一个图的数据结构,然后广度遍历这个图找到最短路径,路径的起始就是GCRoot对象,结束就是泄漏的对象。


具体分析可见:Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理

https://zhuanlan.zhihu.com/p/360944586


3.6 泄漏结果存储与通知


结果的存储与通知主要在DefaultOnHeapAnalyzedListener中完成。


override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
    SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }

    val db = LeaksDbHelper(application).writableDatabase
    val id = HeapAnalysisTable.insert(db, heapAnalysis)
    db.releaseReference()
    ...

    if (InternalLeakCanary.formFactor == TV) {
      showToast(heapAnalysis)
      printIntentInfo()
    } else {
      showNotification(screenToShow, contentTitle)
    }
  }

主要做了两件事:


1.存储泄漏分析结果到数据库中。


2.展示通知,提醒用户去查看内存泄漏情况。


4
为什么LeakCanary不能用于线上?


理解了LeakCanary判定对象泄漏后所做的工作后就不难知道,直接将LeakCanary应用于线上会有如下一些问题:


1.每次内存泄漏以后,都会生成一个.hprof文件,然后解析,并将结果写入.hprof.result。增加手机负担,引起手机卡顿等问题。


2.多次调用GC,可能会对线上性能产生影响。


3.同样的泄漏问题,会重复生成 .hprof 文件,重复分析并写入磁盘。


4..hprof文件较大,信息回捞成问题。


了解了这些问题,我们可以尝试提出一些解决方案:


1.可以根据手机信息来设定一个内存阈值 M ,当已使用内存小于 M 时,如果此时有内存泄漏,只将泄漏对象的信息放入内存当中保存,不生成.hprof文件。当已使用大于 M 时,生成.hprof文件。


2.当引用链路相同时,可根据实际情况去重。


3.不直接回捞.hprof文件,可以选择回捞分析的结果。


4.可以尝试将已泄漏对象存储在数据库中,一个用户同一个泄漏只检测一次,减少对用户的影响。


以上想法并没有经过实际验证,仅供读者参考。


总结


当我们引入LeakCanary后,它就会自动安装并且开始分析内存泄漏并报警。


主要分为以下几步:


1.自动安装。


2.检测可能泄漏的对象。


3.堆快照,生成hprof文件。


4.分析hprof文件。


5.对泄漏进行分类并通知。


本文主要梳理了LeakCanary的主要流程与文章开始提出的几个问题,如果对您有所帮助,欢迎点赞~


参考资料

Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理

https://zhuanlan.zhihu.com/p/360944586

从LeakCanary探究线上内存泄漏检测方案

https://juejin.cn/post/6949759784253915172

全新 LeakCanary 2 ! 完全基于 Kotlin 重构升级 !

https://juejin.cn/post/6844903876043210759

为什么使用LeakCanary检测内存泄漏?

https://blog.csdn.net/wangjiang_qianmo/article/details/83069467




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


抱歉,Xposed真的可以为所欲为

玩转Android AOP  ,这3个案例你需要掌握!
我被打脸了!Dialog 对应的 Context 必须是 Activity吗?


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存