leakCanaray V2.5 框架源码解析

项目地址: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官方说明文档

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

推荐阅读更多精彩内容