LeakCanary Shark解析

/ 0评 / 0

内存快照解析

说到内存泄露,Android一直绕不开的就是LeakCanary,对于内存快照的解析,使用的Shark: Smart Heap Analysis Reports for Kotlin。

Shark主要由shark,shark-android,shark-graph,shark-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(heapDumpTimestamp=1731411320659, version=ANDROID, identifierByteSize=4)
HprofHeader.parseHeaderOf(heapDumpFile)
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)
            }
        }
    )
}

创建方法

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文件,每次读取一行,只解析类和字段:

将混淆类名、字段名作为 key,原类名、原字段名作为value存入map集合,在分析出内存泄漏的引用路径类时,将类名和字段名都通过这个map集合去拿到原始类名和字段名即可,即完成混淆后的反解析

LeakCanry在analyzeHeapleakcanary.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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注