写点什么

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

发布于: 2021 年 03 月 30 日

本文介绍了开源 Android 内存泄漏监控工具 LeakCanary2.0 版本的实现原理,同时介绍了新版本新增的 hprof 文件解析模块的实现原理,包括 hprof 文件协议格式、部分实现源码等。


一、概述

LeakCanary 是一款非常常见的内存泄漏检测工具。经过一系列的变更升级,LeakCanary 来到了 2.0 版本。2.0 版本实现内存监控的基本原理和以往版本差异不大,比较重要的一点变化是 2.0 版本使用了自己的 hprof 文件解析器,不再依赖于 HAHA,整个工具使用的语言也由 Java 切换到了 Kotlin。本文结合源码对 2.0 版本的内存泄漏监控基本原理和 hprof 文件解析器实现原理做一个简单地分析介绍。


LeakCanary 官方链接:https://square.github.io/leakcanary/


1.1 新旧差异

1.1.1 .接入方法

新版: 只需要在 gradle 配置即可。

dependencies {  // debugImplementation because LeakCanary should only run in debug builds.  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'}
复制代码

旧版: 1)gradle 配置;2)Application 中初始化 LeakCanary.install(this) 。


敲黑板:

1)Leakcanary2.0 版本的初始化在 App 进程拉起时自动完成;

2)初始化源代码:

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  }  //....}
复制代码

3)原理:ContentProvider 的 onCreate 在 Application 的 onCreate 之前执行,因此在 App 进程拉起时会自动执行 AppWatcherInstaller 的 onCreate 生命周期,利用 Android 这种机制就可以完成自动初始化;

4)拓展:ContentProvider 的 onCreate 方法在主进程中调用,因此一定不要执行耗时操作,不然会拖慢 App 启动速度。

1.1.2 整体功能

Leakcanary2.0 版本开源了自己实现的 hprof 文件解析以及泄漏引用链查找的功能模块(命名为 shark),后续章节会重点介绍该部分的实现原理。


1.2 整体架构

Leakcanary2.0 版本主要增加了 shark 部分。


二、源码分析

LeakCananry 自动检测步骤:

  1. 检测可能泄漏的对象;

  2. 堆快照,生成 hprof 文件;

  3. 分析 hprof 文件;

  4. 对泄漏进行分类。


2.1 检测实现

自动检测的对象包含以下四类:

  • 销毁的 Activity 实例

  • 销毁的 Fragment 实例\

  • 销毁的 View 实例

  • 清除的 ViewModel 实例

另外,LeakCanary 也会检测 AppWatcher 监听的对象:

AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
复制代码

2.1.1 LeakCanary 初始化



AppWatcher.config :其中包含是否监听 Activity、Fragment 等实例的开关;


Activity 的生命周期监听:注册 Application.ActivityLifecycleCallbacks ;


Fragment 的生命周期期监听:同样,注册 FragmentManager.FragmentLifecycleCallbacks ,但 Fragment 较为复杂,因为 Fragment 有三种,即 android.app.Fragment、androidx.fragment.app.Fragment、android.support.v4.app.Fragment,因此需要注册各自包下的 FragmentManager.FragmentLifecycleCallbacks;


ViewModel 的监听:由于 ViewModel 也是 androidx 下面的特性,

因此其依赖 androidx.fragment.app.Fragment 的监听;


监听 Application 的可见性:不可见时触发 HeapDump,检查存活对象是否存在泄漏。有 Activity 触发 onActivityStarted 则程序可见,Activity 触发 onActivityStopped 则程序不可见,因此监听可见性也是注册 Application.ActivityLifecycleCallbacks 来实现的。

//InternalAppWatcher初始化fun install(application: Application) {         ......         val configProvider = { AppWatcher.config }    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)    onAppWatcherInstalled(application)  } //InternalleakCanary初始化override fun invoke(application: Application) {    _application = application    checkRunningInDebuggableBuild()     AppWatcher.objectWatcher.addOnObjectRetainedListener(this)     val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))     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    )    //Application 可见性监听    application.registerVisibilityListener { applicationVisible ->      this.applicationVisible = applicationVisible      heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)    }    registerResumedActivityListener(application)    addDynamicShortcut(application)     disableDumpHeapInTests()  }
复制代码

2.1.2 如何检测泄漏

1)对象的监听者 ObjectWatcher

ObjectWatcher 的关键代码:

@Synchronized fun watch(    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)    }  }
复制代码

关键类 KeyedWeakReference:弱引用 WeakReference 和 ReferenceQueue 的联合使用,参考 KeyedWeakReference 的父类 WeakReference 的构造方法。


这种使用可以实现如果弱引用关联的的对象被回收,则会把这个弱引用加入到 queue 中,利用这个机制可以在后续判断对象是否被回收。 


2)检测留存的对象

private fun checkRetainedObjects(reason: String) {    val config = configProvider()    // A tick will be rescheduled when this is turned back on.    if (!config.dumpHeap) {      SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }      return    }     //第一次移除不可达对象    var retainedReferenceCount = objectWatcher.retainedObjectCount     if (retainedReferenceCount > 0) {        //主动出发GC      gcTrigger.runGc()        //第二次移除不可达对象      retainedReferenceCount = objectWatcher.retainedObjectCount    }     //判断是否还有剩余的监听对象存活,且存活的个数是否超过阈值    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return     ....     SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }    dismissRetainedCountNotification()    dumpHeap(retainedReferenceCount, retry = true)  }
复制代码

检测主要步骤:

  • 第一次移除不可达对象:移除 ReferenceQueue 中记录的 KeyedWeakReference 对象(引用着监听的对象实例);

  • 主动触发 GC:回收不可达的对象;

  • 第二次移除不可达对象:经过一次 GC 后可以进一步导致只有 WeakReference 持有的对象被回收,因此再一次移除 ReferenceQueue 中记录的 KeyedWeakReference 对象;

  • 判断是否还有剩余的监听对象存活,且存活的个数是否超过阈值;

  • 若满足上面的条件,则抓取 Hprof 文件,实际调用的是 android 原生的 Debug.dumpHprofData(heapDumpFile.absolutePath) ;

  • 启动异步的 HeapAnalyzerService 分析 hprof 文件,找到泄漏的 GcRoot 链路,这个也是后面的主要内容。

//HeapDumpTriggerprivate fun dumpHeap(    retainedReferenceCount: Int,    retry: Boolean  ) {         ....          HeapAnalyzerService.runAnalysis(application, heapDumpFile)  }
复制代码

2.2 Hprof 文件解析

解析入口:

//HeapAnalyzerServiceprivate fun analyzeHeap(    heapDumpFile: File,    config: Config  ): HeapAnalysis {    val heapAnalyzer = HeapAnalyzer(this)     val proguardMappingReader = try {        //解析混淆文件      ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))    } catch (e: IOException) {      null    }    //分析hprof文件    return heapAnalyzer.analyze(        heapDumpFile = heapDumpFile,        leakingObjectFinder = config.leakingObjectFinder,        referenceMatchers = config.referenceMatchers,        computeRetainedHeapSize = config.computeRetainedHeapSize,        objectInspectors = config.objectInspectors,        metadataExtractor = config.metadataExtractor,        proguardMapping = proguardMappingReader?.readProguardMapping()    )  }
复制代码

关于 Hprof 文件的解析细节,就需要牵扯到 Hprof 二进制文件协议:

http://hg.openjdk.java.net/jdk6/jdk6/jdk/raw-file/tip/src/share/demo/jvmti/hprof/manual.html#mozTocId848088


通过阅读协议文档,hprof 的二进制文件结构大概如下:

解析流程:


fun analyze(   heapDumpFile: File,   leakingObjectFinder: LeakingObjectFinder,   referenceMatchers: List<ReferenceMatcher> = emptyList(),   computeRetainedHeapSize: Boolean = false,   objectInspectors: List<ObjectInspector> = emptyList(),   metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,   proguardMapping: ProguardMapping? = null ): HeapAnalysis {   val analysisStartNanoTime = System.nanoTime()    if (!heapDumpFile.exists()) {     val exception = IllegalArgumentException("File does not exist: $heapDumpFile")     return HeapAnalysisFailure(         heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),         HeapAnalysisException(exception)     )   }    return try {     listener.onAnalysisProgress(PARSING_HEAP_DUMP)     Hprof.open(heapDumpFile)         .use { hprof ->           val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)//建立gragh           val helpers =             FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)           helpers.analyzeGraph(//分析graph               metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime           )         }   } catch (exception: Throwable) {     HeapAnalysisFailure(         heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),         HeapAnalysisException(exception)     )   } }
复制代码

LeakCanary 在建立对象实例 Graph 时,主要解析以下几种 tag:

涉及到的 GCRoot 对象有以下几种:

2.2.1 构建内存索引(Graph 内容索引)

LeakCanary 会根据 Hprof 文件构建一个 HprofHeapGraph 对象,该对象记录了以下成员变量:

interface HeapGraph {  val identifierByteSize: Int  /**   * In memory store that can be used to store objects this [HeapGraph] instance.   */  val context: GraphContext  /**   * All GC roots which type matches types known to this heap graph and which point to non null   * references. You can retrieve the object that a GC Root points to by calling [findObjectById]   * with [GcRoot.id], however you need to first check that [objectExists] returns true because   * GC roots can point to objects that don't exist in the heap dump.   */  val gcRoots: List<GcRoot>  /**   * Sequence of all objects in the heap dump.   *   * This sequence does not trigger any IO reads.   */  val objects: Sequence<HeapObject>  //所有对象的序列,包括类对象、实例对象、对象数组、原始类型数组   val classes: Sequence<HeapClass>   //类对象序列   val instances: Sequence<HeapInstance>   //实例对象数组   val objectArrays: Sequence<HeapObjectArray>  //对象数组序列     val primitiveArrays: Sequence<HeapPrimitiveArray>   //原始类型数组序列}
复制代码


为了方便快速定位到对应对象在 hprof 文件中的位置,

LeakCanary 提供了内存索引 HprofInMemoryIndex :


  1. 建立字符串索引 hprofStringCache(Key-value):key 是字符 ID,value 是字符串;

  2. 建立类名索引 classNames(Key-value):key 是类对象 ID,value 是类字符串 ID;

  3. 建立实例索引 instanceIndex(Key-value):key 是实例对象 ID,value 是该对象在 hprof 文件中的位置以及类对象 ID;

  4. 建立类对象索引 classIndex(Key-value):key 是类对象 ID,value 是其他字段的二进制组合(父类 ID、实例大小等等);

  5. 建立对象数组索引 objectArrayIndex(Key-value):key 是类对象 ID,value 是其他字段的二进制组合(hprof 文件位置等等);

  6. 建立原始数组索引 primitiveArrayIndex(Key-value):key 是类对象 ID,value 是其他字段的二进制组合(hprof 文件位置、元素类型等等);


2.2.2 找到泄漏的对象

1)由于需要检测的对象被

com.squareup.leakcanary.KeyedWeakReference 持有,所以可以根据

com.squareup.leakcanary.KeyedWeakReference 类名查询到类对象 ID;

2) 解析对应类的实例域,找到字段名以及引用的对象 ID,即泄漏的对象 ID;


2.2.3 找到最短的 GCRoot 引用链

根据解析到的 GCRoot 对象和泄露的对象,在 graph 中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

//PathFinderprivate fun State.findPathsFromGcRoots(): PathFindingResults {    enqueueGcRoots()//1     val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()    visitingQueue@ while (queuesNotEmpty) {      val node = poll()//2       if (checkSeen(node)) {//2        throw IllegalStateException(            "Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"        )      }       if (node.objectId in leakingObjectIds) {//3        shortestPathsToLeakingObjects.add(node)        // Found all refs, stop searching (unless computing retained size)        if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {//4          if (computeRetainedHeapSize) {            listener.onAnalysisProgress(FINDING_DOMINATORS)          } else {            break@visitingQueue          }        }      }       when (val heapObject = graph.findObjectById(node.objectId)) {//5        is HeapClass -> visitClassRecord(heapObject, node)        is HeapInstance -> visitInstance(heapObject, node)        is HeapObjectArray -> visitObjectArray(heapObject, node)      }    }    return PathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)  }
复制代码


1)GCRoot 对象都入队;

2)队列中的对象依次出队,判断对象是否访问过,若访问过,则抛异常,若没访问过则继续;

3)判断出队的对象 id 是否是需要检测的对象,若是则记录下来,若不是则继续;

4)判断已记录的对象 ID 数量是否等于泄漏对象的个数,若相等则搜索结束,相反则继续;

5)根据对象类型(类对象、实例对象、对象数组对象),按不同方式访问该对象,解析对象中引用的对象并入队,并重复 2)。


入队的元素有相应的数据结构 ReferencePathNode ,原理是链表,可以用来反推出引用链。


三、总结


Leakcanary2.0 较之前的版本最大变化是改由 kotlin 实现以及开源了自己实现的 hprof 解析的代码,总体的思路是根据 hprof 文件的二进制协议将文件的内容解析成一个图的数据结构,当然这个结构需要很多细节的设计,本文并没有面面俱到,然后广度遍历这个图找到最短路径,路径的起始就是 GCRoot 对象,结束就是泄漏的对象。至于泄漏的对象的识别原理和之前的版本并没有差异。


作者:vivo 互联网客户端团队-Li Peidong


用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理