Android内存泄露检测 LeakCanary2.0(Kotlin版)的实现原理

一、概述

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 sealedclassAppWatcherInstaller:ContentProvider(){/**

  * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.

  */internalclassMainProcess:AppWatcherInstaller()  /**  *Whenusingthe`leakcanary-android-process`artifactinsteadof`leakcanary-android`,  * [LeakCanaryProcess]automaticallysetsuptheLeakCanarycode*/internalclassLeakCanaryProcess:AppWatcherInstaller()overridefunonCreate():Boolean{    val application = context!!.applicationContext as Application    AppWatcher.manualInstall(application)returntrue}//....}

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初始化funinstall(application: Application){    ......    val configProvider = { AppWatcher.config }    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)    onAppWatcherInstalled(application)  }//InternalleakCanary初始化override funinvoke(application: Application){    _application =applicationcheckRunningInDebuggableBuild()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 的关键代码:

@Synchronizedfunwatch(

    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)检测留存的对象

privatefuncheckRetainedObjects(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}//第一次移除不可达对象varretainedReferenceCount = objectWatcher.retainedObjectCountif(retainedReferenceCount >0){//主动出发GCgcTrigger.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链路,这个也是后面的主要内容。

//HeapDumpTriggerprivatefundumpHeap(

    retainedReferenceCount: Int,

    retry: Boolean

  ){  ....    HeapAnalyzerService.runAnalysis(application, heapDumpFile)  }

2.2 Hprof 文件解析

解析入口:

//HeapAnalyzerServiceprivatefunanalyzeHeap(

    heapDumpFile: File,

    config: Config

  ): HeapAnalysis{    val heapAnalyzer = HeapAnalyzer(this)    val proguardMappingReader =try{//解析混淆文件ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))    }catch(e: IOException) {null}//分析hprof文件returnheapAnalyzer.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的二进制文件结构大概如下:

need-to-insert-img

need-to-insert-img

解析流程:


need-to-insert-img

funanalyze(

  heapDumpFile: File,

  leakingObjectFinder: LeakingObjectFinder,

  referenceMatchers: List<ReferenceMatcher> = emptyList(),  computeRetainedHeapSize: Boolean=false,  objectInspectors: List = 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")returnHeapAnalysisFailure(        heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),        HeapAnalysisException(exception)    )  }returntry{    listener.onAnalysisProgress(PARSING_HEAP_DUMP)    Hprof.open(heapDumpFile)        .use { hprof ->          val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)//建立graghval helpers =            FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)          helpers.analyzeGraph(//分析graphmetadataExtractor, 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 GLOBALJNI中的全局变量对象ID、jni全局变量引用的对象ID

ROOT JNI LOCALJNI中的局部变量和参数对象ID、线程序列号、栈帧号

ROOT JAVA FRAMEJava 栈帧对象ID、线程序列号、栈帧号

ROOT NATIVE STACKnative方法的出入参数对象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 对象,该对象记录了以下成员变量:

interfaceHeapGraph{  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/**

  * Sequence of all objects in the heap dump.

  *

  * This sequence does not trigger any IO reads.

  */val objects: Sequence//所有对象的序列,包括类对象、实例对象、对象数组、原始类型数组val classes: Sequence//类对象序列val instances: Sequence//实例对象数组val objectArrays: Sequence//对象数组序列val primitiveArrays: Sequence//原始类型数组序列}

为了方便快速定位到对应对象在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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

//PathFinderprivatefun State.findPathsFromGcRoots(): PathFindingResults {    enqueueGcRoots()//1val shortestPathsToLeakingObjects = mutableListOf()    visitingQueue@while(queuesNotEmpty) {      val node = poll()//2if(checkSeen(node)) {//2throwIllegalStateException("Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued")      }if(node.objectId in leakingObjectIds) {//3shortestPathsToLeakingObjects.add(node)// Found all refs, stop searching (unless computing retained size)if(shortestPathsToLeakingObjects.size == leakingObjectIds.size) {//4if(computeRetainedHeapSize) {            listener.onAnalysisProgress(FINDING_DOMINATORS)          }else{break@visitingQueue}        }      }      when (val heapObject = graph.findObjectById(node.objectId)) {//5is HeapClass -> visitClassRecord(heapObject, node)        is HeapInstance -> visitInstance(heapObject, node)        is HeapObjectArray -> visitObjectArray(heapObject, node)      }    }returnPathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)  }

1)GCRoot对象都入队;

2)队列中的对象依次出队,判断对象是否访问过,若访问过,则抛异常,若没访问过则继续;

3)判断出队的对象id是否是需要检测的对象,若是则记录下来,若不是则继续;

4)判断已记录的对象ID数量是否等于泄漏对象的个数,若相等则搜索结束,相反则继续;

5)根据对象类型(类对象、实例对象、对象数组对象),按不同方式访问该对象,解析对象中引用的对象并入队,并重复2)。

入队的元素有相应的数据结构ReferencePathNode ,原理是链表,可以用来反推出引用链。

三、总结

Leakcanary2.0较之前的版本最大变化是改由kotlin实现以及开源了自己实现的hprof解析的代码,总体的思路是根据hprof文件的二进制协议将文件的内容解析成一个图的数据结构,当然这个结构需要很多细节的设计,本文并没有面面俱到,然后广度遍历这个图找到最短路径,路径的起始就是GCRoot对象,结束就是泄漏的对象。至于泄漏的对象的识别原理和之前的版本并没有差异。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容