找回密码
 会员注册
查看: 23|回复: 0

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

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72898
发表于 2024-10-4 16:30:49 | 显示全部楼层 |阅读模式
Li Peidong本文介绍了开源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)对象的监听者ObjectWatcherObjectWatcher 的关键代码:@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链路,这个也是后面的主要内容。//HeapDumpTriggerprivate fun dumpHeap( retainedReferenceCount: Int, retry: Boolean ) { .... HeapAnalyzerService.runAnalysis(application, heapDumpFile) }2.2 Hprof 文件解析解析入口://HeapAnalyzerServiceprivate 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 = 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") 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、堆栈序列号、类名字符串IDCLASS DUMP类快照类对象ID、堆栈序列号、父类对象ID、类加载器对象ID、signers object ID、protection domain object ID、2个reserved、对象大小(byte)、常量池、静态域、实例域INSTANCE DUMP对象实例快照对象ID、堆栈序列号、类对象ID、实例字段所占大小(byte)、实例各字段的值OBJECT ARRAY DUMP对象数组快照数组对象ID、堆栈序列号、元素个数、数组类对象ID、各个元素对象的IDPRIMITIVE ARRAY DUMP原始类型数组快照数组对象ID、堆栈序列号、元素个数、元素类型、各个元素各个GCRoot__涉及到的GCRoot对象有以下几种:TAG备注内容ROOT UNKNOWN_对象IDROOT JNI GLOBALJNI中的全局变量对象ID、jni全局变量引用的对象IDROOT JNI LOCALJNI中的局部变量和参数对象ID、线程序列号、栈帧号ROOT JAVA FRAMEJava 栈帧对象ID、线程序列号、栈帧号ROOT NATIVE STACKnative方法的出入参数对象ID、线程序列号ROOT STICKY CLASS粘性类对象IDROOT THREAD BLOCK线程block对象ID、线程序列号ROOT MONITOR USED被调用了wait()或者notify()或者被synchronized同步的对象对象IDROOT 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 /** * 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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的://PathFinderprivate fun State.findPathsFromGcRoots(): PathFindingResults { enqueueGcRoots()//1 val shortestPathsToLeakingObjects = mutableListOf() 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对象,结束就是泄漏的对象。至于泄漏的对象的识别原理和之前的版本并没有差异。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-12 06:46 , Processed in 0.883252 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表