Android开发者是时候了解LeakCanary了

20次阅读

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

原理概述

关于 LeakCanary 的原理,官网上已经给出了详细的解释。翻译过来就是:1.LeakCanary 使用 ObjectWatcher 来监控 Android 的生命周期。当 Activity 和 Fragment 被 destroy 以后,这些引用被传给 ObjectWatcher 以 WeakReference 的形式引用着。如果 gc 完 5 秒钟以后这些引用还没有被清除掉,那就是内存泄露了。
2. 当被泄露掉的对象达到一个阈值,LeakCanary 就会把 java 的堆栈信息 dump 到.hprof 文件中。
3.LeakCanary 用 Shark 库来解析.hprof 文件,找到无法被清理的引用的引用栈,然后再根据对 Android 系统的知识来判定是哪个实例导致的泄露。
4. 通过泄露信息,LeakCanary 会将一条完整的引用链缩减到一个小的引用链,其余的因为这个小的引用链导致的泄露链都会被聚合在一起。

通过官网的介绍,我们很容易就抓住了学习 LeakCanary 这个库的重点:

  • LeakCanary 是如何使用 ObjectWatcher 监控生命周期的?
  • LeakCanary 如何 dump 和分析.hprof 文件的?

看官方原理总是感觉不过瘾,下面我们从代码层面上来分析。本文基于 LeakCanary 2.0 beta 版。

基本使用

LeakCanary 的使用相当的简单。只需要在 module 的 build.gradle 添加一行依赖,代码侵入少。

dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.0-beta-3’
}dependencies {// debugImplementation because LeakCanary should only run in debug builds. debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.0-beta-3’}

就这样,应用非常简单就接入了 LeakCanary 内存检测功能。当然还有一些更高级的用法,比如更改自定义 config,增加监控项等,大家可以参考官网

源码分析

1. 初始化

和之前的 1.x 版本相比,2.0 甚至都不需要再在 Application 里面增加 install 的代码。可能很多的同学都会疑惑,LeakCanary 是如何插入自己的初始化代码的呢? 其实这里 LeakCanary 是使用了 ContentProvider 来进行初始化。我之前在介绍 Android 插件化系列三:技术流派和四大组件支持的时候曾经介绍过 ContentProvider 的特点,即在打包的过程中来自不同 module 的 ContentProvider 最后都会 merge 到一个文件中,启动 app 的时候 ContentProvider 是自动安装,并且安装会比 Application 的 onCreate 还早。LeakCanary 就是依据这个原理进行的设计。具体可以参考【译】你的 Android 库是否还在 Application 中初始化?

我们可以查看 LeakCanary 源码,发现它在 leakcanary-object-watcher-android 的 AndroidManifest.xml 中有一个 ContentProvider。

<provider

    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
    android:authorities="${applicationId}.leakcanary-installer"
    android:exported="false"/><provider        android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"        android:authorities="${applicationId}.leakcanary-installer"        android:exported="false"/>

然后我们查看 AppWatcherInstaller 的代码,发现内部是使用 InternalAppWatcher 进行的 install。

// AppWatcherInstaller
override fun onCreate(): Boolean {

val application = context!!.applicationContext as Application
InternalAppWatcher.install(application)
return true

}

// InternalAppWatcher
fun install(application: Application) {

// 省略部分代码
checkMainThread()
if (this::application.isInitialized) {return}
InternalAppWatcher.application = application

val configProvider = {AppWatcher.config}
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)

} // AppWatcherInstalleroverride fun onCreate(): Boolean { val application = context!!.applicationContext as Application InternalAppWatcher.install(application) return true} // InternalAppWatcherfun install(application: Application) {// 省略部分代码 checkMainThread() if (this::application.isInitialized) {return} InternalAppWatcher.application = application val configProvider = {AppWatcher.config} ActivityDestroyWatcher.install(application, objectWatcher, configProvider) FragmentDestroyWatcher.install(application, objectWatcher, configProvider) onAppWatcherInstalled(application)}

可以看到这里主要把 Activity 和 Fragment 区分了开来,然后分别进行注册。Activity 的生命周期监听是借助于 Application.ActivityLifecycleCallbacks。

private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() { override fun onActivityDestroyed(activity: Activity) {if (configProvider().watchActivities) {objectWatcher.watch(activity) } } }application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)

而 Fragment 的生命周期监听是借助了 Activity 的 ActivityLifecycleCallbacks 生命周期回调,当 Activity 创建的时候去调用 FragmentManager.registerFragmentLifecycleCallbacks 方法注册 Fragment 的生命周期监听。

override fun onFragmentViewDestroyed(

  fm: FragmentManager,
  fragment: Fragment
) {
  val view = fragment.view
  if (view != null && configProvider().watchFragmentViews) {objectWatcher.watch(view)
  }
}

override fun onFragmentDestroyed(
  fm: FragmentManager,
  fragment: Fragment
) {if (configProvider().watchFragments) {objectWatcher.watch(fragment)
  }
}

} override fun onFragmentViewDestroyed(fm: FragmentManager, fragment: Fragment) {val view = fragment.view if (view != null && configProvider().watchFragmentViews) {objectWatcher.watch(view) } } override fun onFragmentDestroyed(fm: FragmentManager, fragment: Fragment) {if (configProvider().watchFragments) {objectWatcher.watch(fragment) } } }

最终,Activity 和 Fragment 都将自己的引用传入了 ObjectWatcher.watch()进行监控。从这里开始进入到 LeakCanary 的引用监测逻辑。

题外话:LeakCanary 2.0 版本和 1.0 版本相比,增加了 Fragment 的生命周期监听,每个类的职责也更加清晰。但是我个人觉得使用 (Activty)->Unit 这种 lambda 表达式作为类的写法不是很优雅,倒不如面向接口编程。完全可以设计成 ActivityWatcher 和 FragmentWatcher 都继承自某个接口,这样也方便后续扩展。

2. 引用监控

2.1 引用和 GC

  1. 引用
    首先我们先介绍一点准备知识。大家都知道,java 中存在四种引用:
  • 强引用:垃圾回收器绝不会回收它,当内存空间不足,Java 虚拟机宁愿抛出 OOM
  • 软引用:只有在内存不足的时候 JVM 才会回收仅有软引用指向的对象所占的空间
  • 弱引用:当 JVM 进行垃圾回收时,无论内存是否充足,都会回收仅被弱引用关联的对象。
  • 虚引用:和没有任何引用一样,在任何时候都可能被垃圾回收。

一个对象在被 gc 的时候,如果发现还有软引用(或弱引用,或虚引用)指向它,就会在回收对象之前,把这个引用加入到与之关联的引用队列 (ReferenceQueue) 中去。如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。

当软引用(或弱引用,或虚引用)对象所指向的对象被回收了,那么这个引用对象本身就没有价值了,如果程序中存在大量的这类对象(注意,我们创建的软引用、弱引用、虚引用对象本身是个强引用,不会自动被 gc 回收),就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身。

比如我们经常看到这种用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);

还有也有这样一种用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list, new ReferenceQueue<WeakReference<ArrayList>>());

这样就可以把对象和 ReferenceQueue 关联起来,进行对象是否 gc 的判断了。另外我们从弱引用的特征中看到,弱引用是不会影响到这个对象是否被 gc 的,很适合用来监控对象的 gc 情况。

2.GC
java 中有两种手动调用 GC 的方式。

System.gc();
// 或者
Runtime.getRuntime().gc();System.gc();// 或者 Runtime.getRuntime().gc();

2.2 监控

我们在第一节中提到,Activity 和 Fragment 都依赖于响应的 LifecycleCallback 来回调销毁信息,然后调用了 ObjectWatcher.watch 添加了销毁后的监控。接下来我们看 ObjectWatcher.watch 做了什么操作。

@Synchronized fun watch(

watchedObject: Any,
name: String

) {

removeWeaklyReachableObjects()
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
  KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
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)

}

@Synchronized private fun moveToRetained(key: String) {

removeWeaklyReachableObjects()
val retainedRef = watchedObjects\[key\]
if (retainedRef != null) {retainedRef.retainedUptimeMillis = clock.uptimeMillis()
  onObjectRetainedListeners.forEach {it.onObjectRetained() }
}

} @Synchronized fun watch(watchedObject: Any, name: String) {removeWeaklyReachableObjects() val key = UUID.randomUUID().toString() val watchUptimeMillis = clock.uptimeMillis() val reference = KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue) 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) } @Synchronized private fun moveToRetained(key: String) {removeWeaklyReachableObjects() val retainedRef = watchedObjects[key] if (retainedRef != null) {retainedRef.retainedUptimeMillis = clock.uptimeMillis() onObjectRetainedListeners.forEach {it.onObjectRetained() } } }

这里我们看到,有一个存储着 KeyedWeakReference 的 ReferenceQueue 对象。在每次增加 watch object 的时候,都会去把已经处于 ReferenceQueue 中的对象给从监控对象的 map 即 watchObjects 中清理掉,因为这些对象都已经被回收了。然后再去生成一个 KeyedWeakReference,这个对象就是一个持有了 key 和监测开始时间的 WeakReference 对象。最后再去调用 moveToRetained,相当于记录和回调给监控方这个对象正式开始监测的时间。

那么我们现在已经拿到了需要监控的对象了,但是又是怎么去判断这个对象已经内存泄露的呢?这就要继续往下面看。我们主要到前面在讲解 InternalAppWatcher 的 install 方法的时候,除了 install 了 Activity 和 Fragment 的检测器,还调用了 onAppWatcherInstalled(application)方法,看代码发现这个方法就是 InternalLeakCanary 的 invoke 方法。

override fun invoke(application: Application) {

this.application = application

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)

val gcTrigger = GcTrigger.Default

val configProvider = {LeakCanary.config}

val handlerThread = HandlerThread(LEAK\_CANARY\_THREAD\_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)

heapDumpTrigger = HeapDumpTrigger(
    application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
    configProvider
)

}

override fun onObjectRetained() {

if (this::heapDumpTrigger.isInitialized) {heapDumpTrigger.onObjectRetained()
}

} override fun invoke(application: Application) {this.application = application AppWatcher.objectWatcher.addOnObjectRetainedListener(this) val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) val gcTrigger = GcTrigger.Default val configProvider = {LeakCanary.config} val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME) handlerThread.start() val backgroundHandler = Handler(handlerThread.looper) heapDumpTrigger = HeapDumpTrigger(application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper, configProvider) } override fun onObjectRetained() { if (this::heapDumpTrigger.isInitialized) {heapDumpTrigger.onObjectRetained() } }

我们看到首先是初始化了 heapDumper,gcTrigger,heapDumpTrigger 等对象用于 gc 和 heapDump,同时还实现了 OnObjectRetainedListener,并把自己添加到了上面的 onObjectRetainedListeners 中,以便每个对象 moveToRetained 的时候,InternalLeakCanary 都能获取到 onObjectRetained()的回调,回调里就只是回调了 heapDumpTrigger.onObjectRetained()方法。看来都是依赖于 HeapDumpTrigger 这个类。

HeapDumpTrigger 主要的处理逻辑都在 checkRetainedObjects 方法中。

private fun checkRetainedObjects(reason: String) {

val config = configProvider()
var retainedReferenceCount = objectWatcher.retainedObjectCount

if (retainedReferenceCount > 0) {gcTrigger.runGc()  // 触发一次 GC 操作,只保留不能被回收的对象
  retainedReferenceCount = objectWatcher.retainedObjectCount
}

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

if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {showRetainedCountWithDebuggerAttached(retainedReferenceCount)
  scheduleRetainedObjectCheck("debugger was attached", WAIT\_FOR\_DEBUG\_MILLIS)
  return
}

val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
dismissRetainedCountNotification()
val heapDumpFile = heapDumper.dumpHeap()
if (heapDumpFile == null) {scheduleRetainedObjectCheck("failed to dump heap", WAIT\_AFTER\_DUMP\_FAILED\_MILLIS)
  showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
  return
}
lastDisplayedRetainedObjectCount = 0
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)

HeapAnalyzerService.runAnalysis(application, heapDumpFile)

} private fun checkRetainedObjects(reason: String) {val config = configProvider() var retainedReferenceCount = objectWatcher.retainedObjectCount if (retainedReferenceCount > 0) {gcTrigger.runGc() // 触发一次 GC 操作,只保留不能被回收的对象 retainedReferenceCount = objectWatcher.retainedObjectCount } if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {showRetainedCountWithDebuggerAttached(retainedReferenceCount) scheduleRetainedObjectCheck(“debugger was attached”, WAIT_FOR_DEBUG_MILLIS) return } val heapDumpUptimeMillis = SystemClock.uptimeMillis() KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis dismissRetainedCountNotification() val heapDumpFile = heapDumper.dumpHeap() if (heapDumpFile == null) {scheduleRetainedObjectCheck(“failed to dump heap”, WAIT_AFTER_DUMP_FAILED_MILLIS) showRetainedCountWithHeapDumpFailed(retainedReferenceCount) return } lastDisplayedRetainedObjectCount = 0 objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) HeapAnalyzerService.runAnalysis(application, heapDumpFile) }

那么 HeapDumpTrigger 具体做了些啥呢?我理了一下主要是下面几个功能:

  • 后台线程轮询当前还存活着的对象
  • 如果存活的对象大于 0,那就触发一次 GC 操作,回收掉没有泄露的对象
  • GC 完后,仍然存活着的对象数和预定的对象数相比较,如果多了就调用 heapDumper.dumpHeap()方法把对象 dump 成文件,并交给 HeapAnalyzerService 去分析
  • 根据存活情况展示通知

2.3 总结

看到了这里,我们应该脑海中有概念了。Activity 和 Fragment 通过注册系统的监听在 onDestroy 的时候把自己的引用放入 ObjectWatcher 进行监测,监测主要是通过 HeapDumpTrigger 类轮询进行,主要是调用 AndroidHeapDumper 来 dump 出文件来,然后依赖于 HeapAnalyzerService 来进行分析。后面一小节,我们将会聚焦于对象 dump 操作和 HeapAnalyzerService 的分析过程。

3. dump 对象及分析

3.1 dump 对象

hprof 是 JDK 提供的一种 JVM TI Agent native 工具。JVM TI,全拼是 JVM Tool interface,是 JVM 提供的一套标准的 C /C++ 编程接口,是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现。hprof 工具事实上也是实现了这套接口,可以认为是一套简单的 profiler agent 工具。我们在新知周推:10.8-10.14(启动篇)中也提到过,可以参考其中美团的文章。

用过 Android Studio Profiler 工具的同学对 hprof 文件都不会陌生,当我们使用 Memory Profiler 工具的 Dump Java heap 图标的时候,profiler 工具就会去捕获你的内存分配情况。但是捕获以后,只有在 Memory Profiler 正在运行的时候我们才能查看,那么我们要怎么样去保存当时的内存使用情况呢,又或者我想用别的工具来分析堆分配情况呢,这时候 hprof 文件就派上用场了。Android Studio 可以把这些对象给 export 到 hprof 文件中去。

LeakCanary 也是使用的 hprof 文件进行对象存储。hprof 文件比较简单,整体按照 前置信息 + 记录表的格式来组织的。但是记录的种类相当之多。具体种类可以查看 HPROF Agent。

同时,android 中也提供了一个简便的方法 Debug.dumpHprofData(filePath)可以把对象 dump 到指定路径下的 hprof 文件中。LeakCanary 使用使用 Shark 库来解析 Hprof 文件中的各种 record,比较高效,使用 Shark 中的 HprofReader 和 HprofWriter 来进行读写解析,获取我们需要的信息。大家可以关注一些比较重要的,比如:

  • Class Dump
  • Instance Dump
  • Object Array Dump
  • Primitive Array Dump

dump 具体的代码在 AndroidHeapDumper 类中。HprofReader 和 HprofWriter 过于复杂,有兴趣的直接查看源码吧

override fun dumpHeap(): File? {

val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null

return try {Debug.dumpHprofData(heapDumpFile.absolutePath)
  if (heapDumpFile.length() == 0L) {null} else {heapDumpFile}
} catch (e: Exception) {null} finally {}

} override fun dumpHeap(): File? { val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null return try {Debug.dumpHprofData(heapDumpFile.absolutePath) if (heapDumpFile.length() == 0L) {null} else {heapDumpFile} } catch (e: Exception) {null} finally {}}

3.2 对象分析

前面我们已经分析到了,HeapDumpTrigger 主要是依赖于 HeapAnalyzerService 进行分析。那么这个 HeapAnalyzerService 究竟有什么玄机?让我们继续往下面看。可以看到 HeapAnalyzerService 其实是一个 ForegroundService。在接收到分析的 Intent 后就会调用 HeapAnalyzer 的 analyze 方法。所以最终进行分析的地方就是 HeapAnalyzer 的 analyze 方法。

核心代码如下

try {

  listener.onAnalysisProgress(PARSING\_HEAP\_DUMP)
  Hprof.open(heapDumpFile)
      .use { hprof ->
       // 1. 生成 graph
        val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
        // 2. 寻找 Leak
        val findLeakInput = FindLeakInput(graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors)
        val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks()
        listener.onAnalysisProgress(REPORTING\_HEAP\_ANALYSIS)
        return HeapAnalysisSuccess(heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
            applicationLeaks, libraryLeaks
        )
      }
} catch (exception: Throwable) {listener.onAnalysisProgress(REPORTING\_HEAP\_ANALYSIS)
  return HeapAnalysisFailure(heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
      HeapAnalysisException(exception)
  )
}

} try {listener.onAnalysisProgress(PARSING_HEAP_DUMP) Hprof.open(heapDumpFile) .use {hprof -> // 1. 生成 graph val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping) // 2. 寻找 Leak val findLeakInput = FindLeakInput(graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors) val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks() listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS) return HeapAnalysisSuccess(heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime), applicationLeaks, libraryLeaks ) } } catch (exception: Throwable) {listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS) return HeapAnalysisFailure(heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime), HeapAnalysisException(exception) ) } }

这段代码中涉及到了专为 LeakCanary 设计的 Shark 库的用法,在这里就不多解释了。大概介绍一下每一步的作用:

  • 首先调用 HprofHeapGraph.indexHprof 方法,这个方法会把 dump 出来的各种实例 instance,Class 类对象和 Array 对象等都建立起查询的索引,以 record 的 id 作为 key,把需要的信息都存储在 Map 中便于后续取用
  • 调用 findLeakInput.findLeak 方法,这个方法会从 GC Root 开始查询,找到最短的一条导致泄露的引用链,然后再根据这条引用链构建出 LeakTrace。
  • 把查询出来的 LeakTrace 对外展示

总结

本篇文章分析了 LeakCanary 检测内存泄露的思路和一些代码的设计思想,但是限于篇幅不能面面俱到。接下来我们回答一下文章开头提出的问题。

1.LeakCanary 是如何使用 ObjectWatcher 监控生命周期的?
LeakCanary 使用了 Application 的 ActivityLifecycleCallbacks 和 FragmentManager 的 FragmentLifecycleCallbacks 方法进行 Activity 和 Fragment 的生命周期检测,当 Activity 和 Fragment 被回调 onDestroy 以后就会被 ObjectWatcher 生成 KeyedReference 来检测,然后借助 HeapDumpTrigger 的轮询和触发 gc 的操作找到弹出提醒的时机。

2.LeakCanary 如何 dump 和分析.hprof 文件的?
使用 Android 平台自带的 Debug.dumpHprofData 方法获取到 hprof 文件,使用自建的 Shark 库进行解析,获取到 LeakTrace

转载 https://mp.weixin.qq.com/s/pl…

正文完
 0