美文网首页Android
leakCanaray V2.5 框架源码解析

leakCanaray V2.5 框架源码解析

作者: Stan_Z | 来源:发表于2020-12-09 21:58 被阅读0次

    项目地址:https://github.com/square/leakcanary/tree/v2.5
    官方使用说明:https://square.github.io/leakcanary/

    一、使用

    1.1 工程引入
    2.0之后的版本,不需要在application中配置LeakCanary.install(this),只在build.gradle配置引入库即可:

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

    运行项目如果有如下log打印,证明leakCanaray已经安装好,能正常运行了:

    D LeakCanary: LeakCanary is running and ready to detect leaks
    

    1.2 触发场景
    Activity、Fragment、Fragment View实例被销毁,ViewModel被清理场景下会自动触发检测。

    1.3 自动检测和上报工作流
    监控对象回收情况->如有泄漏dump heap ->分析heap ->输出结果。

    1.4 局限性:无法检测根Activity及Service。
    因为接入和测试成本低,因此比较推荐使用它对常规业务的内存泄漏问题做一个初步筛查。

    2.0之前版本的使用参考之前文章:性能优化工具(九)-LeakCanary

    二、源码分析

    2.1 初始化

    因为没有了LeakCanary.install(this),且类名发生了变化,所以框架初始化的地方有点难找,全局搜索install,还真能找到。(注:leakCanaray V2.5是kotlin代码)

    internal sealed class AppWatcherInstaller : ContentProvider() {
    override fun onCreate(): Boolean {
       val application = context!!.applicationContext as Application
       AppWatcher.manualInstall(application)
       return true
    }
    }
    

    这里初始化是在ContentProvider.onCreate,它执行在application.onCreate之前,因此省略了在客户端application install的步骤。接着看:AppWatcher.manualInstall ->InternalAppWatcher.install

    leakcanary/internal/InternalAppWatcher.kt
    
    fun install(application: Application) {
      checkMainThread()
      if (this::application.isInitialized) {
       return
      }
      InternalAppWatcher.application = application
      if (isDebuggableBuild) {
       SharkLog.logger = DefaultCanaryLog()
      }
    
      val configProvider = { AppWatcher.config }
      ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
      FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
      onAppWatcherInstalled(application)
    }
    

    这里分别对Activity和Fragment进行了install.

    2.2 内存泄漏监控

    这里以 ActivityDestroyWatcher.install为例分析

    ActivityDestroyWatcher.kt
    
    internal class ActivityDestroyWatcher private constructor(
      private val objectWatcher: ObjectWatcher,
      private val configProvider: () -> Config
    ) {
      private val lifecycleCallbacks =
       object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
         override fun onActivityDestroyed(activity: Activity) {
           if (configProvider().watchActivities) {
             objectWatcher.watch(
                 activity, "${activity::class.java.name} received Activity#onDestroy() callback"
             )
           }
         }
       }
    
      companion object {
       fun install(
         application: Application,
         objectWatcher: ObjectWatcher,
         configProvider: () -> Config
       ) {
         val activityDestroyWatcher =
           ActivityDestroyWatcher(objectWatcher, configProvider)
         application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
       }
      }
    }
    

    初始化ActivityDestroyWatcher,并且向application统一注册生命周期回调,监听到Activity onDestroy回调,通过ObjectWatcher.watch来实现内存泄漏监控。

    leakcanary/ObjectWatcher.kt
    
    private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
    private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
    private val queue = ReferenceQueue<Any>()
    @Synchronized fun watch(
      watchedObject: Any,
      description: String
    ) {
      if (!isEnabled()) {
       return
      }
    
      //1.先把gc前ReferenceQueue中的引用清除
      removeWeaklyReachableObjects()
      val key = UUID.randomUUID()
         .toString()
      val watchUptimeMillis = clock.uptimeMillis()
      //2.将activity引起包装为弱引用,并与ReferenceQueue建立关联
      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
      //3\. 5s之后出发检测(5s时间内gc完成)
      checkRetainedExecutor.execute {
       moveToRetained(key)
      }
    }
    

    这里checkRetainedExecutor是外部传入的,有5s延迟执行。

    leakcanary/internal/InternalAppWatcher.kt
    
    private val checkRetainedExecutor = Executor {
      mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)//5s
    }
    

    接着往下看:

    private fun moveToRetained(key: String) {
       removeWeaklyReachableObjects()
       val retainedRef = watchedObjects[key]
       if (retainedRef != null) {
           retainedRef.retainedUptimeMillis = clock.uptimeMillis()
           onObjectRetainedListeners.forEach { it.onObjectRetained() }
       }
    }
    

    5s延迟时间内,如果gc回收成功,retainedRef则为null,否则则触发内存泄漏处理,当然5s之内也不一定会触发gc,所以之后的内存泄漏处理会主动gc再判断一次。

    leakcanary/internal/InternalLeakCanary.kt
    
    override fun onObjectRetained() = scheduleRetainedObjectCheck()
    fun scheduleRetainedObjectCheck() {
      if (this::heapDumpTrigger.isInitialized) {
       heapDumpTrigger.scheduleRetainedObjectCheck()
      }
    }
    

    这里主要是确认下是否存在内存泄漏,逻辑不细看了,这里最终会执行dumpHeap:

    2.3 dump确认

    leakcanary/internal/HeapDumpTrigger.kt
    
    private fun dumpHeap(
      retainedReferenceCount: Int,
      retry: Boolean
    ) {
      saveResourceIdNamesToMemory()
      val heapDumpUptimeMillis = SystemClock.uptimeMillis()
      KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
       //1.dump heap
      when (val heapDumpResult = heapDumper.dumpHeap()) {
    ...
       is HeapDump -> {
    …
       //2.analysis heap
         HeapAnalyzerService.runAnalysis(
             context = application,
             heapDumpFile = heapDumpResult.file,
             heapDumpDurationMillis = heapDumpResult.durationMillis
         )
       }
      }
    }
    

    这里主要就是dump hprof文件,然后起个服务来分析dump heap文件。

    2.4 heap dump

    leakcanary/internal/AndroidHeapDumper.kt
    
    override fun dumpHeap(): DumpHeapResult {
      val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump
      val waitingForToast = FutureResult<Toast?>()
      showToast(waitingForToast)
      if (!waitingForToast.wait(5, SECONDS)) {
       SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
       return NoHeapDump
      }
    
      val notificationManager =
       context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
      if (Notifications.canShowNotification) {
       val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
       val builder = Notification.Builder(context)
           .setContentTitle(dumpingHeap)
       val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
       notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
      }
    
      val toast = waitingForToast.get()
      return try {
       val durationMillis = measureDurationMillis {
         Debug.dumpHprofData(heapDumpFile.absolutePath)
       }
    
       if (heapDumpFile.length() == 0L) {
         SharkLog.d { "Dumped heap file is 0 byte length" }
         NoHeapDump
       } else {
         HeapDump(file = heapDumpFile, durationMillis = durationMillis)
       }
      } catch (e: Exception) {
       SharkLog.d(e) { "Could not dump heap" }
       // Abort heap dump
       NoHeapDump
      } finally {
       cancelToast(toast)
       notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
      }
    }
    

    这里很简单,dump过程先发出Notification,再通过Debug.dumpHprofData dump hprof文件。

    cepheus:/data/data/com.example.leakcanary/files/leakcanary # ls -al
    -rw------- 1 u0_a260 u0_a260 22944796 2020-12-07 11:30 2020-12-07_11-30-37_701.hprof
    -rw------- 1 u0_a260 u0_a260 21910520 2020-12-07 14:52 2020-12-07_14-52-40_703.hprof
    

    接下来看service的分析工作

    2.5 hprof内存泄漏分析

    leakcanary/internal/HeapAnalyzerService.kt
    
    override fun onHandleIntentInForeground(intent: Intent?) {
      if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) {
       SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." }
       return
      }
    
      // Since we're running in the main process we should be careful not to impact it.
      Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
      val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File
      val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS, -1)
      val config = LeakCanary.config
      val heapAnalysis = if (heapDumpFile.exists()) {
       analyzeHeap(heapDumpFile, config)
      } else {
       missingFileFailure(heapDumpFile)
      }
      val fullHeapAnalysis = when (heapAnalysis) {
       is HeapAnalysisSuccess -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
       is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
      }
      onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
      config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
    }
    

    首先这个服务是新起了进程来处理的

    <service
       android:name="leakcanary.internal.HeapAnalyzerService"
       android:exported="false"
       android:process=":leakcanary" />
    

    这里核心方法应该在analyzeHeap

    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
      }
      return heapAnalyzer.analyze(
          heapDumpFile = heapDumpFile,
         leakingObjectFinder = config.leakingObjectFinder,
         referenceMatchers = config.referenceMatchers,
         computeRetainedHeapSize = config.computeRetainedHeapSize,
         objectInspectors = config.objectInspectors,
         metadataExtractor = config.metadataExtractor,
         proguardMapping = proguardMappingReader?.readProguardMapping()
      )
    }
    

    那么最终分析heap dumps找出泄漏点的工作是交给HeapAnalyzer来处理的

    shark/HeapAnalyzer.kt
    
    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 = heapDumpFile,
           createdAtTimeMillis = System.currentTimeMillis(),
           analysisDurationMillis = since(analysisStartNanoTime),
           exception = HeapAnalysisException(exception)
        )
      }
      return try {
        listener.onAnalysisProgress(PARSING_HEAP_DUMP)
        val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile))
        sourceProvider.openHeapGraph(proguardMapping).use { graph ->
         val helpers =
            FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
          val result = helpers.analyzeGraph(
              metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
          )
          val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
          val randomAccessStats =
            "RandomAccess[" +
                "bytes=${sourceProvider.randomAccessByteReads}," +
                "reads=${sourceProvider.randomAccessReadCount}," +
                "travel=${sourceProvider.randomAccessByteTravel}," +
                "range=${sourceProvider.byteTravelRange}," +
                "size=${heapDumpFile.length()}" +
                "]"
         val stats = "$lruCacheStats $randomAccessStats"
         result.copy(metadata = result.metadata + ("Stats" to stats))
        }
      } catch (exception: Throwable) {
        HeapAnalysisFailure(
            heapDumpFile = heapDumpFile,
           createdAtTimeMillis = System.currentTimeMillis(),
           analysisDurationMillis = since(analysisStartNanoTime),
           exception = HeapAnalysisException(exception)
        )
      }
    }
    

    这里通过ConstantMemoryMetricsDualSourceProvider读取hprof文件,然后由FindLeakInput来进行分析。

    private fun FindLeakInput.analyzeGraph(
      metadataExtractor: MetadataExtractor,
      leakingObjectFinder: LeakingObjectFinder,
      heapDumpFile: File,
      analysisStartNanoTime: Long
    ): HeapAnalysisSuccess {
      listener.onAnalysisProgress(EXTRACTING_METADATA)
      val metadata = metadataExtractor.extractMetadata(graph)
      listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS)
      //1.从hprof中获取泄漏的对象id集合,这里主要是收集没有被回收的弱引用。
      val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
      //2.针对这些疑似泄漏的对象,计算到gcroot的最短引用路径,确定是否发生泄漏。
      val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds)
      return HeapAnalysisSuccess(
          heapDumpFile = heapDumpFile,
         createdAtTimeMillis = System.currentTimeMillis(),
         analysisDurationMillis = since(analysisStartNanoTime),
         metadata = metadata,
         applicationLeaks = applicationLeaks,
         libraryLeaks = libraryLeaks
      )
    }
    

    这里leakingObjectFinder.findLeakingObjectIds实际上是KeyedWeakReferenceFinder,先通过它来获取泄漏对象的id集合。然后通过findLeaks针对这些疑似泄漏的对象,计算到gcroot的最短引用路径,确定是否发生泄漏。

    最后构建LeakTrace,传递引用链,呈现分析结果。

    val leakTrace = LeakTrace(
        gcRootType = GcRootType.fromGcRoot(shortestPath.root.gcRoot),
       referencePath = referencePath,
       leakingObject = leakTraceObjects.last()
    )
    
    三、框架变迁

    官方说明:

    image.png

    从1.6.3版本开始,有比较大的变化,简单总结起来:

    • java切到kotlin

    • heap分析库从haha转为Shark,haha本身也是square的开源库:https://github.com/square/haha,Shark没有作为三方开源库独立存在,而是leakCanaray的一个组件,因此新项目总体kotlin代码量增加了。

    • 对内存泄漏工作流做了优化。

    四、工作流总结

    这里以Activity为例,简单对leakCanary核心类关系做下整理,leackCanaray还提供了FragmentDestroyWatcher,这里就不分析了,原理应该是一样的。

    leakCanary核心类参与的工作流梳理
    • 应用进程部分主要是对Activity/Fragment生命周期监控,watcher他们的引用。

    • 内存泄漏预判检测机制:通过WeakReference +ReferenceQueue来判断对象是否被系统GC回收,Activity/Fragment引用被包装为WeakReference,同时传入ReferenceQueue。当被包装的Activity/Fragment对象生命周期结束,被gc检测到,则会将它添加到 ReferenceQueue 中,等ReferenceQueue处理。当 GC 过后对象一直不被加入 ReferenceQueue,它可能存在内存泄漏。

    • 是否触发dump操作逻辑:这里会主动触发一次gc,再来看看是否有没被回收的弱引用对象。应用在前台,需要满足5个及以上泄漏对象才触发dump操作,后台满足1个就行,但是前后台均还会收一个nonpReason的制约,这个reason相当于一个统一的容错,保存判断leakCanary是否安装、配置是否正确、之前的noify通知发没发等等。

    • dump hprof文件通过 Debug.dumpHprofData(filePath)来实现,在data/data/package/files/leakcanary 目录下,文件大小10几M到几十M不等。这个过程应该是耗时的。

    • hprof文件分析工作交给HeapAnalyserService来处理,它本身在一个单独进程中,核心功能通过Shark来完成,内存泄漏主要工作:从hprof中获取泄漏的对象id集合,这里主要是收集没有被回收的弱引用,针对这些疑似泄漏的对象,计算到gcroot的最短引用路径,确认是否发生泄漏。如果确认有内存泄漏,则会生成统计报表输出。

    参考:leakcanary官方说明文档

    相关文章

      网友评论

        本文标题:leakCanaray V2.5 框架源码解析

        本文链接:https://www.haomeiwen.com/subject/ctyfgktx.html