内存快照解析
说到内存泄露,Android一直绕不开的就是LeakCanary,对于内存快照的解析,使用的Shark: Smart Heap Analysis Reports for Kotlin。
Shark主要由shark,shark-android,shark-graph,shark-hprof等模块组成
其中各个模块作用如下:
- shark-hprof:主要用于读写hprof文件中的Record数据
- shark-graph:主要用于生成堆对象的关系图
- shark:主要用于生成hprof文件的解析报告
- shark-android:生成Android平台定制的堆对象分析类
- shark-cli:解析hprof文件的命令行工具
引入依赖如下
def sharkVersion = '2.14'
implementation "com.squareup.leakcanary:shark-hprof:$sharkVersion"
implementation "com.squareup.leakcanary:shark-graph:$sharkVersion"
implementation "com.squareup.leakcanary:shark:$sharkVersion"
implementation "com.squareup.leakcanary:shark-android:$sharkVersion"
LeakCanary内存快照解析
相关代码在如下位置
leakcanary.internal.AndroidDebugHeapAnalyzer#analyzeHeap
主要是调用Shark的shark.HeapAnalyzer#analyze
方法
LeakCanary堆栈还原
通过查看leakcanary.internal.AndroidDebugHeapAnalyzer#analyzeHeap
中的方法,可以发现将mapping文件重命名为leakCanaryObfuscationMapping.txt
并放在assets目录下,那么release包也能自动还原。具体的实现在shark.ProguardMappingReader#readProguardMapping
Shark使用方法
对象介绍
HprofHeader
:hprof文件的头部信息
//HprofHeader(heapDumpTimestamp=1731411320659, version=ANDROID, identifierByteSize=4)
HprofHeader.parseHeaderOf(heapDumpFile)
Hprof
: 表示hprof文件,可以用来读取hprof文件中的记录
Hprof.open(heapDumpFile).use { hprof ->
hprof.reader.readHprofRecords(
// 指定我们只对StringRecord类型的记录感兴趣
recordTypes = setOf(HprofRecord.StringRecord::class),
// 设置一个监听器来接收并处理每一个StringRecord记录
listener = OnHprofRecordListener { _, record ->
if (record is HprofRecord.StringRecord) {
println(record.string)
}
}
)
}
HeapGraph
: 用于表示 heap 中的对象关系图,主要是通过此对象可以对内存中的对象进行分析
创建方法
val heapDumpFile = File("1731411311203.hprof")
//直接使用HprofHeapGraph中针对File对象的扩展方法获取
val heapGraph1 = heapDumpFile.openHeapGraph()
//推荐:使用包装方法获取
val heapGraph2 = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)).openHeapGraph()
//使用如下方式在解析的时候可以取消
ConstantMemoryMetricsDualSourceProvider(ThrowingCancelableFileSourceProvider(heapDumpFile) {
if (需要取消) {
throw RuntimeException("Analysis canceled")
}
}).openHeapGraph()
当我们获取到HeapGraph对象后,可以对HeapGraph对象进行分析。常用方法如下
//过滤出指定类的对象实例
heapGraph.findClassByName("java.lang.Thread")
//直接获取所有对象实例
heapGraph.instances
//判断对象是否为指定类型
heapGraph.instances.filter { it.instanceOf("android.app.Activit") }
//读取对象的成员变量
heapGraph.instances.filter { it.instanceOf("android.app.Activit") }
.forEach {
val mDestroyed = it.readField("android.app.Activity", "mDestroyed")
//转换为具体类型
if (mDestroyed?.value?.asBoolean == true) {
println(it)
}
}
示例:获取当前快照中所有的线程信息
val openHeapGraph = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)).openHeapGraph()
openHeapGraph.use { heapGraph ->
val heapClass = heapGraph.findClassByName("java.lang.Thread")
heapClass?.instances?.map {
val nameField = it.readField("java.lang.Thread", "name")!!
nameField.value.readAsJavaString()!!
}?.forEach { println(it) }
}
输出所有bitmap的宽高
val openHeapGraph = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)).openHeapGraph()
openHeapGraph.use { heapGraph ->
val bitmapClass = heapGraph.findClassByName("android.graphics.Bitmap")
bitmapClass?.instances?.forEach { instance ->
val widthField = instance.readField("android.graphics.Bitmap", "mWidth")?.value?.asInt
val heightField = instance.readField("android.graphics.Bitmap", "mHeight")?.value?.asInt
println("width = ${widthField},height = $heightField")
}
}
//获取应用版本号 shark.AndroidBuildMirror
class AndroidBuildMirror(
/**
* Value of android.os.Build.MANUFACTURER
*/
val manufacturer: String,
/**
* Value of android.os.Build.VERSION.SDK_INT
*/
val sdkInt: Int,
/**
* Value of android.os.Build.ID
*/
val id: String
) {
companion object {
fun fromHeapGraph(graph: HeapGraph): AndroidBuildMirror {
//graph.context可以当成缓存使用,往里面塞东西
return graph.context.getOrPut(AndroidBuildMirror::class.java.name) {
val buildClass = graph.findClassByName("android.os.Build")!!
val versionClass = graph.findClassByName("android.os.Build\$VERSION")!!
val manufacturer = buildClass["MANUFACTURER"]!!.value.readAsJavaString()!!
val sdkInt = versionClass["SDK_INT"]!!.value.asInt!!
val id = buildClass["ID"]!!.value.readAsJavaString()!!
AndroidBuildMirror(manufacturer, sdkInt, id)
}
}
}
}
另外针对混淆的apk,openHeapGraph
方法可以传入mapping文件做到反混淆
val heapDumpFile = File("1731411311203.hprof")
val mappingPath = "./mapping.txt"
val proguardMappingReader = try {
ProguardMappingReader(File(mappingPath).inputStream())
} catch (e: IOException) {
null
}
val readProguardMapping = proguardMappingReader?.readProguardMapping()
//推荐:使用包装方法获取
val heapGraph = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)).openHeapGraph(readProguardMapping)
原理也很简单,解析mapping.txt文件,每次读取一行,只解析类和字段:
- 类特征 :行尾为 : 冒号结尾,然后根据 -> 作为 index 分割,左边的为原类名,右边的为混淆类名
- 字段特征:行尾不为 : 冒号结尾,并且不包含 (括号(带括号的为方法),即为字段特征,根据 -> 作为 index 分割,左边为原字段名,右边的为混淆字段名
将混淆类名、字段名作为 key,原类名、原字段名作为value存入map集合,在分析出内存泄漏的引用路径类时,将类名和字段名都通过这个map集合去拿到原始类名和字段名即可,即完成混淆后的反解析
HeapAnalyzer
: 用于分析hprof文件中的内存泄露
LeakCanry在analyzeHeap
leakcanary.internal.AndroidDebugHeapAnalyzer#analyzeHeap
中使用HeapAnalyzer进行的内存泄露分析,我们把逻辑改改拿过来就可以本地解析内存泄露了
val heapAnalyzer = HeapAnalyzer(object : OnAnalysisProgressListener {
override fun onAnalysisProgress(step: OnAnalysisProgressListener.Step) {
//解析回调
//step ==> EXTRACTING_METADATA
//step ==> FINDING_RETAINED_OBJECTS
//step ==> FINDING_PATHS_TO_RETAINED_OBJECTS
//step ==> INSPECTING_OBJECTS
//step ==> COMPUTING_NATIVE_RETAINED_SIZE
//step ==> COMPUTING_RETAINED_SIZE
//step ==> BUILDING_LEAK_TRACES
println("step ==> $step")
}
})
heapGraph.use { graph ->
val result = heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
graph = graph,
leakingObjectFinder = FilteringLeakingObjectFinder(
AndroidObjectInspectors.appLeakingObjectFilters
),
referenceMatchers = AndroidReferenceMatchers.appDefaults,
computeRetainedHeapSize = true,
objectInspectors = AndroidObjectInspectors.appDefaults,
metadataExtractor = AndroidMetadataExtractor
)
if (result is HeapAnalysisSuccess) {
val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
val randomAccessStats =
"RandomAccess[" +
"bytes=${sourceProvider.randomAccessByteReads}," +
"reads=${sourceProvider.randomAccessReadCount}," +
"travel=${sourceProvider.randomAccessByteTravel}," +
"range=${sourceProvider.byteTravelRange}," +
"size=${heapDumpFile.length()}" +
"]"
val stats = "$lruCacheStats $randomAccessStats"
result.copy(metadata = result.metadata + ("Stats" to stats))
//解析的结果,和LeakCanary的结果是一样的
println(result)
}
总结
首先通过hprof文件生成HeapGraph。然后通过HeapGraph#findClassByName找到对应的HeapClass(实例对应的类),然后通过HeapClass#instances获取到对应的实例。获取到实例以后,通过readField即可获取实例的属性HeapField。HeapField#value 可以获取到对应的值,然后通过asXXX转换成实际的数据
File.openHeapGraph->HeapGraph.findClassByName->HeapClass.instances->HeapInstance.readField->HeapField->HeapField.vale.asXXX
扩展
通过前面的分析可以知道,通过查看shark.HeapAnalyzer#analyze
方法,可以发现,获取引用链主要使用的是FindLeakInput,但是因为shark中将相关类标记为private所以没法直接调用。koom基于shark做了一个kshark库,将部分api修改为public,所以我们可以使用kshark,使用方式一模一样。
implementation 'com.kuaishou.koom:shark:2.2.2'
val heapDumpFile = File("1.hprof")
val openHeapGraph = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)).openHeapGraph()
val activity = openHeapGraph.findClassByName("android.graphics.Bitmap")
val longSet = activity?.instances?.map { it.objectId }?.toSet()
val heapAnalyzer = HeapAnalyzer(object : OnAnalysisProgressListener {
override fun onAnalysisProgress(step: OnAnalysisProgressListener.Step) {
//step ==> EXTRACTING_METADATA
//step ==> FINDING_RETAINED_OBJECTS
//step ==> FINDING_PATHS_TO_RETAINED_OBJECTS
//step ==> INSPECTING_OBJECTS
//step ==> COMPUTING_NATIVE_RETAINED_SIZE
//step ==> COMPUTING_RETAINED_SIZE
//step ==> BUILDING_LEAK_TRACES
println("step ==> $step")
}
})
val findLeakInput = FindLeakInput(
openHeapGraph, AndroidReferenceMatchers.appDefaults,
true, mutableListOf()
)
//必须使用with调用
with(heapAnalyzer) {
val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks(longSet!!)
println(applicationLeaks)
}
当然,推荐是直接把源码复制一份,哪里没法调用改哪里。ps:koom的主要逻辑在com.kwai.koom.javaoom.monitor.analysis.HeapAnalysisService
参考文档
https://juejin.cn/post/7043755844718034958
https://juejin.cn/post/7270831057053761591
https://juejin.cn/post/7018883931067908132