本文介绍了开源 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 自动检测步骤:
- 检测可能透露的对象;
- 堆快照,生成 hprof 文件;
- 剖析 hprof 文件;
- 对透露进行分类。
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:
- 建设字符串索引 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<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