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

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

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查询到类名。

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

    作用: 根据类对象ID查询类字符串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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

//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

Android内存泄露检测 LeakCanary2.0(Kotlin版)的实现原理的更多相关文章

  1. Android内存泄露---检测工具篇

    内存使用是程序开发无法回避的一个问题.如果我们毫不在意肆意使用,总有一天会为此还账,且痛不欲生...所以应当防患于未然,把内存使用细化到平时的每一行代码中. 内存使用概念较大,本篇先讲对已有app如何 ...

  2. 【转】c++内存泄露检测,长文慎入!

    原文网址:http://blog.csdn.net/zengraoli/article/details/8905334 关于内存泄露的,今天无意想到,网上找了一下   本篇blog附带的所有工具和代码 ...

  3. Android内存泄漏检测利器:LeakCanary

    Android内存泄漏检测利器:LeakCanary MAR 28TH, 2016 是什么? 一言以蔽之:LeakCanary是一个傻瓜化并且可视化的内存泄露分析工具 为什么需要LeakCanary? ...

  4. vld(Visual Leak Detector) 内存泄露检测工具

    初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复 杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题.内存 ...

  5. android 内存泄漏检测工具 LeakCanary 泄漏金丝雀

    韩梦飞沙 yue31313 韩亚飞 han_meng_fei_sha 313134555@qq.com 内存泄漏检测工具 android 内存泄漏检测工具 ======== 内存泄漏 就是  无用的对 ...

  6. (转)专项:Android 内存泄露实践分析

    今天看到一篇关于Android 内存泄露实践分析的文章,感觉不错,讲的还算详细,mark到这里. 原文发表于:Testerhome: 作者:ycwdaaaa ;  原文链接:https://teste ...

  7. memwatch内存泄露检测工具

    工具介绍 官网 http://www.linkdata.se/sourcecode/memwatch/ 其功能如下官网介绍,挑选重点整理: 1. 号称功能: 内存泄露检测 (检测未释放内存, 即 动态 ...

  8. Visual C++内存泄露检测—VLD工具使用说明[转]

    Visual C++内存泄露检测—VLD工具使用说明 一.        VLD工具概述 Visual Leak Detector(VLD)是一款用于Visual C++的免费的内存泄露检测工具.他的 ...

  9. Visual C++内存泄露检测—VLD工具使用说明

    一.        VLD工具概述 Visual Leak Detector(VLD)是一款用于Visual C++的免费的内存泄露检测工具.他的特点有:可以得到内存泄漏点的调用堆栈,如果可以的话,还 ...

  10. vld,Bounds Checker,memwatch,mtrace,valgrind,debug_new几种内存泄露检测工具的比较,Valgrind Cheatsheet

    概述 内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,在大型的.复杂的应用程序中,内存泄漏是常见的问题.当以前分配的一片内存不再需要使用或无法访问时,但是却 ...

随机推荐

  1. 【封装】二维BIT

    struct BIT{ #define maxn 1000 int n, m; int d1[maxn][maxn], d2[maxn][maxn], d3[maxn][maxn], d4[maxn] ...

  2. c++学习,和友元函数

    第一友元函数访问私有元素时不会显示,但是是可以调用的(我使用的是gcc10.3版本的)友元函数可以访问任何元素.就是语法你别写错了. 继承如果父类已经写了构造函数,子类一定要赋值给构造函数,要么父类就 ...

  3. Mybatis-Flex之QueryWrapper

    1.完整DQL语句 /** * 使用QueryWrapper构建超复杂SQL语句 */ @Test public void testQueryWrapper1() { QueryWrapper wra ...

  4. 如何使用sharding-sphere完成读写分离和分库分表?

    一.sharding-sphere配置读写分离 1.先搭建好一个MySQL的主从集群,可以参考[MySQL主从集群搭建] 2.在项目中导入相关依赖(记得刷新Maven) <!--读写分离--&g ...

  5. navicat连接服务器mysql

    navicat连接服务器mysql 第一步:配置防火墙 连接服务器的mysql数据库,我们首先需要在服务器上放行3306端口(MySQL服务对应的端口),进入服务器管理页面防火墙,点击添加规则,放行3 ...

  6. 苹果推信群发,苹果推信群发软件,iMessage群发系统

    在当今数字化的时代,智能手机的普及率已达到了前所未有的高度,其中,苹果公司的iPhone无疑是市场上最受欢迎的智能手机之一,然而,与手机的广泛应用相伴的是,众多企业对于如何有效地向这些手机用户推送信息 ...

  7. [简单] 基于注解玩转excel导出导入

    基于注解玩转excel导出导入 demo 请 加入 598347590 技术解决群获取 个人博客 https://yangbuyi.top 话不多说 开始 <!-- poi office --& ...

  8. windows环境下如何优雅搭建ftp服务?

    目录 0. 前言 1.ftp简介 2.下载Apache FTPServer 3.下载并解压压缩包 4.修改配置文件 4.1 修改users.properties配置文件 4.2 修改ftpd-typi ...

  9. 实践案例丨ACL2020 KBQA 基于查询图生成回答多跳复杂问题

    摘要:目前复杂问题包括两种:含约束的问题和多跳关系问题.本文对ACL2020 KBQA 基于查询图生成的方法来回答多跳复杂问题这一论文工作进行了解读,并对相关实验进行了复现. 1.摘要 1.1 复杂问 ...

  10. 《低代码发展白皮书(2022年)》&《2022低代码·无代码应用案例汇编》,发布了

    摘要:信息通信研究院云计算与大数据研究所副所长栗蔚发布了<低代码发展白皮书(2022年)>&<2022低代码·无代码应用案例汇编>. 本文分享自华为云社区<华为参 ...