一、概述

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

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 整体架构

c9ee8a284809ed4adcbe7d60d5da4c29.png

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

二、源码分析

LeakCananry自动检测步骤:

检测可能泄漏的对象;

堆快照,生成hprof文件;

分析hprof文件;

对泄漏进行分类。

2.1 检测实现

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

销毁的Activity实例

销毁的Fragment实例\

销毁的View实例

清除的ViewModel实例

另外,LeakCanary也会检测

AppWatcher 监听的对象:

AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

2.1.1 LeakCanary初始化

cdf34233cc08e4ea87b55e44dabb9d00.png

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

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 的关键代码:

[object Object]@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二进制文件协议:

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

fed3181a8f1ad81e2378847b481ddc36.png

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==解析流程:

9deb1280cb33cd7ca853c29f4772f87e.png

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

[object Object]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:

5852356a768a6cdf8a1374a7268004bd.png

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

00ed9f485622d9d492f0b1419e478ac8.png

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   //原始类型数组序列

}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

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

建立字符串索引

hprofStringCache(Key-value):key是字符ID,value是字符串;

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

建立类名索引

classNames(Key-value):key是类对象ID,value是类字符串ID;

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

建立实例索引

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

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

建立类对象索引

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

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

建立对象数组索引

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

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

建立原始数组索引

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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐