用烂LeakCanary2,隔壁产品看不懂了

1,969 阅读3分钟

1、它是什么?

它是square公司开源的一套内存检测工具。本文基于最新的 2.6 版本。

2、如何使用?

LeakCanary2的引入使用非常简单,build.gradle添加以下依赖即可。

dependencies {  // debugImplementation because LeakCanary should only run in debug builds.  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'}

如果LeakCanary2被顺利初始化,Logcat输出

D LeakCanary: LeakCanary is running and ready to detect leaks

3、看看原理

3.1 初始化

为什么只用添加build.gradle依赖就可以初始化LeakCanary呢?原因在于AndroidManifest清单文件的合并。合并后的AndroidManifest文件如下:

源头在leakcanary-object-watcher-android-2.6/AndroidManifest.xml,声明了ContentProvider。

即AppWatcherInstaller中的MainProcess,原来在ContentProvider的onCreate中调用了初始化方法AppWatcher.manualInstall()

3.2 绑定生命周期

检测是否存在内存泄漏的前提是绑定对应模块的生命周期,在appDefaultWathcers方法中

以Activity的内存泄漏检测为例

3.3 内存泄漏检测

*注:以Activity为例

3.3.1 ActivityWatcher.lifecycleCallbacks

精确判断是否内存泄漏的前提是要获知对应模块的声明周期,以Actviity为例,当Activity触发onDestroy时,回调ObjectWather.expectWeaklyReachable方法。

其中的reachabilityWatcher为ObjectWatcher类型,在manualInstall时初始化。

3.3.2 HeapDumpTrigger.scheduleRetainedObjectCheck

ObjectWather**.expectWeaklyReachable方法将调用moveToRetained**方法,触发OnObjectRetainedListener.onObjectRetained,这里的OnObjectRetainedListener就是InternalLeakCanary,最终调用HeapDumpTrigger.scheduleRetainedObjectCheck方法

private fun checkRetainedObjects() {  val iCanHasHeap = HeapDumpControl.iCanHasHeap()  val config = configProvider()  if (iCanHasHeap is Nope) {    if (iCanHasHeap is NotifyingNope) {      // Before notifying that we can't dump heap, let's check if we still have retained object.      var retainedReferenceCount = objectWatcher.retainedObjectCount      if (retainedReferenceCount > 0) {        gcTrigger.runGc()        retainedReferenceCount = objectWatcher.retainedObjectCount      }      val nopeReason = iCanHasHeap.reason()      val wouldDump = !checkRetainedCount(        retainedReferenceCount, config.retainedVisibleThreshold, nopeReason      )      if (wouldDump) {        val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)        onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))        showRetainedCountNotification(          objectCount = retainedReferenceCount,          contentText = uppercaseReason        )      }    } else {      SharkLog.d {        application.getString(          R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()        )      }    }    return  }  var retainedReferenceCount = objectWatcher.retainedObjectCount  if (retainedReferenceCount > 0) {    gcTrigger.runGc()    retainedReferenceCount = objectWatcher.retainedObjectCount  }  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return  val now = SystemClock.uptimeMillis()  val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis  if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {    onRetainInstanceListener.onEvent(DumpHappenedRecently)    showRetainedCountNotification(      objectCount = retainedReferenceCount,      contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)    )    scheduleRetainedObjectCheck(      delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis    )    return  }  dismissRetainedCountNotification()  val visibility = if (applicationVisible) "visible" else "not visible"  dumpHeap(    retainedReferenceCount = retainedReferenceCount,    retry = true,    reason = "$retainedReferenceCount retained objects, app is $visibility"  )}


代码比较长,大致逻辑为手动触发GcTrigger.runGc后,根据引用计数判断是否需要dump内存,如果需要则调用dumpHeap方法。由于系统GC不可控,LeakCanary在手动GC后,等待 5s 再次检查引用计数。

3.3.3 HeapDumpTrigger.dumpHeap

内部调用AndroidHeapDumper.dumpHeap方法,本质还是使用了Debug类的dumpHprofData方法。

3.3.4 HeapAnalyzerService.runAnalysis、HeapAnalyzer.analyze

HeapAnalyzerService为IntentService类型,内部调用HeapAnalyzer.analyze方法,最后使用shark库中的openHeapGraph方法分析hprof文件,返回分析结果。

3.4 shark浅析

shark出世之前,LeakCanary使用的是haha库(现已弃用)。shark是LeakCanary2中自带的内存分析工具,全称为Smart Heap Analysis Reports for Kotlin。可以单独使用该库实现“自研的内存检测工具”。也可使用CLI(Command Line Interface)版本做竞品分析。

dependencies {  
    implementation 'com.squareup.leakcanary:shark-graph:$sharkVersion'
}

3.4.1 简单使用

MainActivity中定义了constant变量,使用shark分析hprof文件,取出constant的值

代码如下:

Debug.dumpHprofData("/sdcard/testDump.hprof")val dumpFile: File = File("/sdcard/testDump.hprof")val constants = dumpFile.openHeapGraph()    .findClassByName(MainActivity::class.java.name)?.instances?.map { instance ->        val temp = instance[MainActivity::class.java.name, "constant"]!!        temp.value.asInt    }constants?.forEach {    Log.i("MainActivity", "constant==>" + it)}

运行结果:

3.4.2 关键方法

通过File的openHeapGraph扩展方法,最终会调用HprofIndex.indexRecordsOf方法,该方法用于根据hprof文件内容创建内存索引。借用美团的Probe中的图,hprof文件格式大体如下

indexRecordsOf方法如下,可以看到调用了readRecords方法,根据不同的tag进行计数。

fun indexHprof(  reader: StreamingHprofReader,  hprofHeader: HprofHeader,  proguardMapping: ProguardMapping?,  indexedGcRootTags: Set<HprofRecordTag>): HprofInMemoryIndex {    //省略...  val bytesRead = reader.readRecords(    EnumSet.of(CLASS_DUMP, INSTANCE_DUMP, OBJECT_ARRAY_DUMP, PRIMITIVE_ARRAY_DUMP),    OnHprofRecordTagListener { tag, _, reader ->      val bytesReadStart = reader.bytesRead      when (tag) {        CLASS_DUMP -> {          //省略...        }        INSTANCE_DUMP -> {          //省略...        }        OBJECT_ARRAY_DUMP -> {          //省略...        }        PRIMITIVE_ARRAY_DUMP -> {          //省略...        }      }    })  //省略...  val recordTypes = EnumSet.of(    STRING_IN_UTF8,    LOAD_CLASS,    CLASS_DUMP,    INSTANCE_DUMP,    OBJECT_ARRAY_DUMP,    PRIMITIVE_ARRAY_DUMP  ) + HprofRecordTag.rootTags.intersect(indexedGcRootTags)  reader.readRecords(recordTypes, indexBuilderListener)  return indexBuilderListener.buildIndex(proguardMapping, hprofHeader)}


在readRecords方法中找到各个根引用类型,通过listener回调给调用者,方便调用者建立对应的索引,代码节选如下:

根应用类型如下,具体见hprof文件格式文档,在参考链接中。

值得注意的是,该方法本质上是对文件内容的读取,文件的读取涉及Java IO,它是如何读取的呢?

BufferedSource有点眼熟,继续看

果然是OkIO中的BufferedSource,OkIO相关的文章在这:

《用烂OkIO,隔壁产品看不懂了》

3.4.3 流程总结

Debug.dumpHprof->File.openHeapGraph->HprofIndex.indexRecordsOf->HprofInMemoryIndex.indexHprof->OkIO.BufferedSource取流->回调给调用者建立索引->从HprofHeapGraph中findClassByName

4、适用场景

  • 线下内存监控,检测内存泄漏

5、注意事项

如果要满足线上内存监控场景,需要对hprof文件进行裁剪,核心思想在于只读取感兴趣的文件内容或者只写入感兴趣的文件内容。

国际惯例,文章末尾列出工程地址

点我点我点我

参考:

[hprof文件格式]:hg.openjdk.java.net/jdk6/jdk6/j…

[LeakCanary官方文档]:square.github.io/leakcanary/