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

本文介绍了开源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链路,这个也是后面的主要内容。
//HeapDumpTrigger
private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean
  ) {

   ....

    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

2.2 Hprof 文件解析

解析入口:

//HeapAnalyzerService
private 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 = emptyList(),
   computeRetainedHeapSize: Boolean = false,
   objectInspectors: List = 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:

TAG 含义 内容
STRING 字符串 字符ID、字符串内容
LOAD CLASS 已加载的类 序列号、类对象ID、堆栈序列号、类名字符串ID
CLASS DUMP 类快照 类对象ID、堆栈序列号、父类对象ID、类加载器对象ID、signers object ID、protection domain object ID、2个reserved、对象大小(byte)、常量池、静态域、实例域
INSTANCE DUMP 对象实例快照 对象ID、堆栈序列号、类对象ID、实例字段所占大小(byte)、实例各字段的值
OBJECT ARRAY DUMP 对象数组快照 数组对象ID、堆栈序列号、元素个数、数组类对象ID、各个元素对象的ID
PRIMITIVE ARRAY DUMP 原始类型数组快照 数组对象ID、堆栈序列号、元素个数、元素类型、各个元素
各个GCRoot

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

TAG 备注 内容
ROOT UNKNOWN 对象ID
ROOT JNI GLOBAL JNI中的全局变量 对象ID、jni全局变量引用的对象ID
ROOT JNI LOCAL JNI中的局部变量和参数 对象ID、线程序列号、栈帧号
ROOT JAVA FRAME Java 栈帧 对象ID、线程序列号、栈帧号
ROOT NATIVE STACK native方法的出入参数 对象ID、线程序列号
ROOT STICKY CLASS 粘性类 对象ID
ROOT THREAD BLOCK 线程block 对象ID、线程序列号
ROOT MONITOR USED 被调用了wait()或者notify()或者被synchronized同步的对象 对象ID
ROOT THREAD OBJECT 启动并且没有stop的线程 线程对象ID、线程序列号、堆栈序列号

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
  /**
   * Sequence of all objects in the heap dump.
   *
   * This sequence does not trigger any IO reads.
   */
  val objects: Sequence  //所有对象的序列,包括类对象、实例对象、对象数组、原始类型数组
 
  val classes: Sequence   //类对象序列
 
  val instances: Sequence   //实例对象数组
 
  val objectArrays: Sequence  //对象数组序列

  val primitiveArrays: Sequence   //原始类型数组序列
}

为了方便快速定位到对应对象在hprof文件中的位置,LeakCanary提供了内存索引HprofInMemoryIndex :

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

作用: 可以根据类名,查询到字符ID,也可以根据字符ID查询到类名。

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

作用: 根据类对象ID查询类字符串ID。

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

作用: 快速定位实例的所处位置,方便解析实例字段的值。

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

作用: 快速定位类对象的所处位置,方便解析类字段类型。

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

作用: 快速定位对象数组的所处位置,方便解析对象数组引用的对象。

  1. 建立原始数组索引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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

//PathFinder
private fun State.findPathsFromGcRoots(): PathFindingResults {
    enqueueGcRoots()//1
 
    val shortestPathsToLeakingObjects = mutableListOf()
    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

你可能感兴趣的:(Android内存泄露检测 LeakCanary2.0(Kotlin版)的实现原理)