关于android:Android内存泄露检测-LeakCanary20Kotlin版的实现原理

2次阅读

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

本文介绍了开源 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<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:

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<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 是字符串;

作用:能够依据类名,查问到字符 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<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

正文完
 0