LeakCanary 核心源码分析

LeakCanary工作机制:

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。KeyedWeakReference继承于 WeakReference,并且构造该对象的时候,需要传入ReferenceQueue对象。
    final class KeyedWeakReference extends WeakReference {
      public final String key;
      public final String name;
    
      KeyedWeakReference(Object referent, String key, String name,
          ReferenceQueue referenceQueue) {
        super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
        this.key = checkNotNull(key, "key");
        this.name = checkNotNull(name, "name");
      }
    }
     
     
    1. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
    2. 如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
    3. 在另外一个进程中的 HeapAnalyzerService(此服务在单独的进程中运行,以避免减慢应用进程或使其内存不足), 有一个HeapAnalyzer(分析由 RefWatcher 生成的heap dumps,以验证可疑泄漏是否真实) 使用 Shark(LeakCanary2)、HaHa(LeakCanary1) 解析这个文件。
    4. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
    5. HeapAnalyzer 计算到 GC roots的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
    6. 引用链传递到 APP 进程中的 DisplayLeakService 来记录泄漏分析结果,然后将启动 DisplayLeakActivity, 并以通知的形式展示出来。

    核心解决的问题

    一旦检测到内存泄漏,LeakCanary 就会 dump Memory 信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,极大地方便了 Android 应用程序的开发。

    大致原理

    其思路大致为:监听Activity生命周期->onDestroy以后延迟5秒判断Activity有没有被回收->如果没有回收,调用GC,再此判断是否回收,如果还没回收,则内存泄露了,反之,没有泄露。整个框架最核心的问题就是在什么时间点如何判断一个Activity是否被回收了。

    LeakCanary2 和 LeakCanary1,有什么不一样

    • 完全使用 Kotlin 重写。
    • 使用新的Heap分析工具 Shark,替换到之前的 haha,按官方的说法,内存占用减少了10倍。
    • 泄露类型分组。
    • LeakCanary2 集成更简单,实现了自动调用 install() 方法,实现方式是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。
    • 开源了自己实现的 hprof 解析的代码,总体的思路是根据 hprof 文件的二进制协议将文件的内容解析成一个图的数据结构,当然这个结构需要很多细节的设计,然后广度遍历这个图找到最短路径,路径的起始就是 GCRoot 对象,结束就是泄漏的对象。至于泄漏的对象的识别原理和之前的版本并没有差异。

    如何检测泄漏

    对象的监听者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 中,利用这个机制可以在后续判断对象是否被回收。

    检测留存的对象:

    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)
      }
    
    //HeapDumpTrigger
    private fun dumpHeap(
        retainedReferenceCount: Int,
        retry: Boolean
      ) {
          
       ....
          
        HeapAnalyzerService.runAnalysis(application, heapDumpFile)
      }
    

    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()
        )
      }
    
    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)
         )
       }
     }
    

    构建内存索引(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 :

    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文件位置、元素类型等等);

    找到泄漏的对象

    根据解析到的GCRoot对象和泄露的对象,在graph中搜索最短引用链,这里采用的是广度优先遍历的算法进行搜索的:

    //PathFinder
    private 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 ,原理是链表,可以用来反推出引用链。

    你可能感兴趣的:(LeakCanary 核心源码分析)