Chinaunix首页 | 论坛 | 博客
  • 博客访问: 909325
  • 博文数量: 253
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2609
  • 用 户 组: 普通用户
  • 注册时间: 2019-03-08 17:29
个人简介

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

文章分类

全部博文(253)

文章存档

2022年(60)

2021年(81)

2020年(83)

2019年(29)

我的朋友

分类: Android平台

2021-03-30 11:39:36

一、概述

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


1.1 新旧差异


1.1.1 .接入方法

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

点击(此处)折叠或打开

  1. dependencies {
  2.   // debugImplementation because LeakCanary should only run in debug builds.
  3.   debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
  4. }

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

敲黑板:

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

2)初始化源代码:

点击(此处)折叠或打开

  1. internal sealed class AppWatcherInstaller : ContentProvider() {
  2.  
  3.   /**
  4.    * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
  5.    */
  6.   internal class MainProcess : AppWatcherInstaller()
  7.  
  8.   /**
  9.    * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
  10.    * [LeakCanaryProcess] automatically sets up the LeakCanary code
  11.    */
  12.   internal class LeakCanaryProcess : AppWatcherInstaller()
  13.  
  14.   override fun onCreate(): Boolean {
  15.     val application = context!!.applicationContext as Application
  16.     AppWatcher.manualInstall(application)
  17.     return true
  18.   }
  19.   //....
  20. }

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 监听的对象:

点击(此处)折叠或打开

  1. 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 来实现的。

点击(此处)折叠或打开

  1. //InternalAppWatcher初始化
  2. fun install(application: Application) {
  3.      
  4.     ......
  5.      
  6.     val configProvider = { AppWatcher.config }
  7.     ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
  8.     FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
  9.     onAppWatcherInstalled(application)
  10.   }
  11.  
  12. //InternalleakCanary初始化
  13. override fun invoke(application: Application) {
  14.     _application = application
  15.     checkRunningInDebuggableBuild()
  16.  
  17.     AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
  18.  
  19.     val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))
  20.  
  21.     val gcTrigger = GcTrigger.Default
  22.  
  23.     val configProvider = { LeakCanary.config }
  24.     //异步线程执行耗时操作
  25.     val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
  26.     handlerThread.start()
  27.     val backgroundHandler = Handler(handlerThread.looper)
  28.  
  29.     heapDumpTrigger = HeapDumpTrigger(
  30.         application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
  31.         configProvider
  32.     )
  33.     //Application 可见性监听
  34.     application.registerVisibilityListener { applicationVisible ->
  35.       this.applicationVisible = applicationVisible
  36.       heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
  37.     }
  38.     registerResumedActivityListener(application)
  39.     addDynamicShortcut(application)
  40.  
  41.     disableDumpHeapInTests()
  42.   }


2.1.2 如何检测泄漏

1)对象的监听者ObjectWatcher

ObjectWatcher 的关键代码:

点击(此处)折叠或打开

  1. @Synchronized fun watch(
  2.     watchedObject: Any,
  3.     description: String
  4.   ) {
  5.     if (!isEnabled()) {
  6.       return
  7.     }
  8.     removeWeaklyReachableObjects()
  9.     val key = UUID.randomUUID()
  10.         .toString()
  11.     val watchUptimeMillis = clock.uptimeMillis()
  12.     val reference =
  13.       KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
  14.     SharkLog.d {
  15.       "Watching " +
  16.           (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
  17.           (if (description.isNotEmpty()) " ($description)" else "") +
  18.           " with key $key"
  19.     }
  20.  
  21.     watchedObjects[key] = reference
  22.     checkRetainedExecutor.execute {
  23.       moveToRetained(key)
  24.     }
  25.   }

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

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


2)检测留存的对象

点击(此处)折叠或打开

  1. private fun checkRetainedObjects(reason: String) {
  2.     val config = configProvider()
  3.     // A tick will be rescheduled when this is turned back on.
  4.     if (!config.dumpHeap) {
  5.       SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
  6.       return
  7.     }
  8.  
  9.     //第一次移除不可达对象
  10.     var retainedReferenceCount = objectWatcher.retainedObjectCount
  11.  
  12.     if (retainedReferenceCount > 0) {
  13.         //主动出发GC
  14.       gcTrigger.runGc()
  15.         //第二次移除不可达对象
  16.       retainedReferenceCount = objectWatcher.retainedObjectCount
  17.     }
  18.  
  19.     //判断是否还有剩余的监听对象存活,且存活的个数是否超过阈值
  20.     if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
  21.  
  22.     ....
  23.  
  24.     SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
  25.     dismissRetainedCountNotification()
  26.     dumpHeap(retainedReferenceCount, retry = true)
  27.   }

检测主要步骤:

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

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

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

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

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

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

点击(此处)折叠或打开

  1. //HeapDumpTrigger
  2. private fun dumpHeap(
  3.     retainedReferenceCount: Int,
  4.     retry: Boolean
  5.   ) {
  6.       
  7.    ....
  8.       
  9.     HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  10.   }


2.2 Hprof 文件解析

解析入口:

点击(此处)折叠或打开

  1. //HeapAnalyzerService
  2. private fun analyzeHeap(
  3.     heapDumpFile: File,
  4.     config: Config
  5.   ): HeapAnalysis {
  6.     val heapAnalyzer = HeapAnalyzer(this)
  7.  
  8.     val proguardMappingReader = try {
  9.         //解析混淆文件
  10.       ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
  11.     } catch (e: IOException) {
  12.       null
  13.     }
  14.     //分析hprof文件
  15.     return heapAnalyzer.analyze(
  16.         heapDumpFile = heapDumpFile,
  17.         leakingObjectFinder = config.leakingObjectFinder,
  18.         referenceMatchers = config.referenceMatchers,
  19.         computeRetainedHeapSize = config.computeRetainedHeapSize,
  20.         objectInspectors = config.objectInspectors,
  21.         metadataExtractor = config.metadataExtractor,
  22.         proguardMapping = proguardMappingReader?.readProguardMapping()
  23.     )
  24.   }

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

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

解析流程:

点击(此处)折叠或打开

  1. fun analyze(
  2.    heapDumpFile: File,
  3.    leakingObjectFinder: LeakingObjectFinder,
  4.    referenceMatchers: List<ReferenceMatcher> = emptyList(),
  5.    computeRetainedHeapSize: Boolean = false,
  6.    objectInspectors: List<ObjectInspector> = emptyList(),
  7.    metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
  8.    proguardMapping: ProguardMapping? = null
  9.  ): HeapAnalysis {
  10.    val analysisStartNanoTime = System.nanoTime()
  11.  
  12.    if (!heapDumpFile.exists()) {
  13.      val exception = IllegalArgumentException("File does not exist: $heapDumpFile")
  14.      return HeapAnalysisFailure(
  15.          heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
  16.          HeapAnalysisException(exception)
  17.      )
  18.    }
  19.  
  20.    return try {
  21.      listener.onAnalysisProgress(PARSING_HEAP_DUMP)
  22.      Hprof.open(heapDumpFile)
  23.          .use { hprof ->
  24.            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)//建立gragh
  25.            val helpers =
  26.              FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
  27.            helpers.analyzeGraph(//分析graph
  28.                metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
  29.            )
  30.          }
  31.    } catch (exception: Throwable) {
  32.      HeapAnalysisFailure(
  33.          heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
  34.          HeapAnalysisException(exception)
  35.      )
  36.    }
  37.  }
LeakCanary在建立对象实例Graph时,主要解析以下几种tag:


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

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

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

点击(此处)折叠或打开

  1. interface HeapGraph {
  2.   val identifierByteSize: Int
  3.   /**
  4.    * In memory store that can be used to store objects this [HeapGraph] instance.
  5.    */
  6.   val context: GraphContext
  7.   /**
  8.    * All GC roots which type matches types known to this heap graph and which point to non null
  9.    * references. You can retrieve the object that a GC Root points to by calling [findObjectById]
  10.    * with [GcRoot.id], however you need to first check that [objectExists] returns true because
  11.    * GC roots can point to objects that don't exist in the heap dump.
  12.    */
  13.   val gcRoots: List<GcRoot>
  14.   /**
  15.    * Sequence of all objects in the heap dump.
  16.    *
  17.    * This sequence does not trigger any IO reads.
  18.    */
  19.   val objects: Sequence<HeapObject> //所有对象的序列,包括类对象、实例对象、对象数组、原始类型数组
  20.  
  21.   val classes: Sequence<HeapClass> //类对象序列
  22.  
  23.   val instances: Sequence<HeapInstance> //实例对象数组
  24.  
  25.   val objectArrays: Sequence<HeapObjectArray> //对象数组序列
  26.    
  27.   val primitiveArrays: Sequence<HeapPrimitiveArray> //原始类型数组序列
  28. }

为了方便快速定位到对应对象在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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

点击(此处)折叠或打开

  1. //PathFinder
  2. private fun State.findPathsFromGcRoots(): PathFindingResults {
  3.     enqueueGcRoots()//1
  4.  
  5.     val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
  6.     visitingQueue@ while (queuesNotEmpty) {
  7.       val node = poll()//2
  8.  
  9.       if (checkSeen(node)) {//2
  10.         throw IllegalStateException(
  11.             "Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"
  12.         )
  13.       }
  14.  
  15.       if (node.objectId in leakingObjectIds) {//3
  16.         shortestPathsToLeakingObjects.add(node)
  17.         // Found all refs, stop searching (unless computing retained size)
  18.         if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {//4
  19.           if (computeRetainedHeapSize) {
  20.             listener.onAnalysisProgress(FINDING_DOMINATORS)
  21.           } else {
  22.             break@visitingQueue
  23.           }
  24.         }
  25.       }
  26.  
  27.       when (val heapObject = graph.findObjectById(node.objectId)) {//5
  28.         is HeapClass -> visitClassRecord(heapObject, node)
  29.         is HeapInstance -> visitInstance(heapObject, node)
  30.         is HeapObjectArray -> visitObjectArray(heapObject, node)
  31.       }
  32.     }
  33.     return PathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)
  34.   }

1)GCRoot对象都入队;

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

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

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

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


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


三、总结

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

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

阅读(2213) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~