• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

Android内存泄露检测-LeakCanary20Kotlin版的实现原理

android 搞代码 4年前 (2022-03-01) 37次浏览 已收录 0个评论
文章目录[隐藏]

本文介绍了开源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配置即可。

<code class="java">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)初始化源代码:

<code class="java">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 监听的对象:

<code class="java">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 来实现的。

<code class="java">//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 的要害代码:

<code class="java">@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)检测留存的对象

<code class="java">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链路,这个也是前面的次要内容。
<code class="java">//HeapDumpTrigger
private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean
  ) {

   ....

    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

2.2 Hprof 文件解析

解析入口:

<code class="java">//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的二进制文件构造大略如下:

解析流程:

<code class="java">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 对象,该对象记录了以下成员变量:

<code class="java">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中搜寻最短援用链,这里采纳的是广度优先遍历的算法进行搜寻的:

<code class="java">//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


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Android内存泄露检测-LeakCanary20Kotlin版的实现原理

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址