zoukankan      html  css  js  c++  java
  • Android内存泄露检测 LeakCanary2.0(Kotlin版)的实现原理

    本文介绍了开源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自动检测步骤:

    1. 检测可能泄漏的对象;

    2. 堆快照,生成hprof文件;

    3. 分析hprof文件;

    4. 对泄漏进行分类。

    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)对象的监听者ObjectWatcher

    ObjectWatcher 的关键代码:

    @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链路,这个也是后面的主要内容。

    //HeapDumpTrigger
    private fun dumpHeap(
        retainedReferenceCount: Int,
        retry: Boolean
      ) {
          
       ....
          
        HeapAnalyzerService.runAnalysis(application, heapDumpFile)
      }
    
    
    

    2.2 Hprof 文件解析

    解析入口:

    //HeapAnalyzerService
    private 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<ReferenceMatcher> = emptyList(),
       computeRetainedHeapSize: Boolean = false,
       objectInspectors: List<ObjectInspector> = 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:

    涉及到的GCRoot对象有以下几种:

    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<GcRoot>
      /**
       * Sequence of all objects in the heap dump.
       *
       * This sequence does not trigger any IO reads.
       */
      val objects: Sequence<HeapObject>  //所有对象的序列,包括类对象、实例对象、对象数组、原始类型数组
     
      val classes: Sequence<HeapClass>   //类对象序列
     
      val instances: Sequence<HeapInstance>   //实例对象数组
     
      val objectArrays: Sequence<HeapObjectArray>  //对象数组序列
       
      val primitiveArrays: Sequence<HeapPrimitiveArray>   //原始类型数组序列
    }
    
    
    
    

    为了方便快速定位到对应对象在hprof文件中的位置,LeakCanary提供了内存索引HprofInMemoryIndex :

    1. 建立字符串索引hprofStringCache(Key-value):key是字符ID,value是字符串;

      作用: 可以根据类名,查询到字符ID,也可以根据字符ID查询到类名。

    2. 建立类名索引classNames(Key-value):key是类对象ID,value是类字符串ID;

      作用: 根据类对象ID查询类字符串ID。

    3. 建立实例索引**instanceIndex(**Key-value):key是实例对象ID,value是该对象在hprof文件中的位置以及类对象ID;

      作用: 快速定位实例的所处位置,方便解析实例字段的值。

    4. 建立类对象索引classIndex(Key-value):key是类对象ID,value是其他字段的二进制组合(父类ID、实例大小等等);

      作用: 快速定位类对象的所处位置,方便解析类字段类型。

    5. 建立对象数组索引objectArrayIndex(Key-value):key是类对象ID,value是其他字段的二进制组合(hprof文件位置等等);

      作用: 快速定位对象数组的所处位置,方便解析对象数组引用的对象。

    6. 建立原始数组索引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中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

    //PathFinder
    private fun State.findPathsFromGcRoots(): PathFindingResults {
        enqueueGcRoots()//1
     
        val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
        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对象,结束就是泄漏的对象。至于泄漏的对象的识别原理和之前的版本并没有差异。

    作者:vivo 互联网客户端团队-Li Peidong

    分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。
  • 相关阅读:
    spring注解事务管理
    Spring切入点表达式常用写法
    JPA和事务管理
    maven仓库
    struts2拦截器
    js中 转义字符
    jquery中变量加$和不加$有什么区别!
    spring大乱炖
    第一章 java多线程
    CS:APP 05 笔记
  • 原文地址:https://www.cnblogs.com/vivotech/p/14595395.html
Copyright © 2011-2022 走看看