leakCanaray V2.5 框架源碼解析

項(xiàng)目地址:https://github.com/square/leakcanary/tree/v2.5
官方使用說明:https://square.github.io/leakcanary/

一、使用

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

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

運(yùn)行項(xiàng)目如果有如下log打印,證明leakCanaray已經(jīng)安裝好,能正常運(yùn)行了:

D LeakCanary: LeakCanary is running and ready to detect leaks

1.2 觸發(fā)場(chǎng)景
Activity、Fragment、Fragment View實(shí)例被銷毀,ViewModel被清理場(chǎng)景下會(huì)自動(dòng)觸發(fā)檢測(cè)。

1.3 自動(dòng)檢測(cè)和上報(bào)工作流
監(jiān)控對(duì)象回收情況->如有泄漏dump heap ->分析heap ->輸出結(jié)果。

1.4 局限性:無法檢測(cè)根Activity及Service。
因?yàn)榻尤牒蜏y(cè)試成本低,因此比較推薦使用它對(duì)常規(guī)業(yè)務(wù)的內(nèi)存泄漏問題做一個(gè)初步篩查。

2.0之前版本的使用參考之前文章:性能優(yōu)化工具(九)-LeakCanary

二、源碼分析

2.1 初始化

因?yàn)闆]有了LeakCanary.install(this),且類名發(fā)生了變化,所以框架初始化的地方有點(diǎn)難找,全局搜索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,它執(zhí)行在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)
}

這里分別對(duì)Activity和Fragment進(jìn)行了install.

2.2 內(nèi)存泄漏監(jiān)控

這里以 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統(tǒng)一注冊(cè)生命周期回調(diào),監(jiān)聽到Activity onDestroy回調(diào),通過ObjectWatcher.watch來實(shí)現(xiàn)內(nèi)存泄漏監(jiān)控。

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建立關(guān)聯(lián)
  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之后出發(fā)檢測(cè)(5s時(shí)間內(nèi)gc完成)
  checkRetainedExecutor.execute {
   moveToRetained(key)
  }
}

這里checkRetainedExecutor是外部傳入的,有5s延遲執(zhí)行。

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延遲時(shí)間內(nèi),如果gc回收成功,retainedRef則為null,否則則觸發(fā)內(nèi)存泄漏處理,當(dāng)然5s之內(nèi)也不一定會(huì)觸發(fā)gc,所以之后的內(nèi)存泄漏處理會(huì)主動(dòng)gc再判斷一次。

leakcanary/internal/InternalLeakCanary.kt

override fun onObjectRetained() = scheduleRetainedObjectCheck()
fun scheduleRetainedObjectCheck() {
  if (this::heapDumpTrigger.isInitialized) {
   heapDumpTrigger.scheduleRetainedObjectCheck()
  }
}

這里主要是確認(rèn)下是否存在內(nèi)存泄漏,邏輯不細(xì)看了,這里最終會(huì)執(zhí)行dumpHeap:

2.3 dump確認(rèn)

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文件,然后起個(gè)服務(wù)來分析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)
  }
}

這里很簡(jiǎn)單,dump過程先發(fā)出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內(nèi)存泄漏分析

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

首先這個(gè)服務(wù)是新起了進(jìn)程來處理的

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

這里核心方法應(yīng)該在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找出泄漏點(diǎn)的工作是交給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來進(jìn)行分析。

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中獲取泄漏的對(duì)象id集合,這里主要是收集沒有被回收的弱引用。
  val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
  //2.針對(duì)這些疑似泄漏的對(duì)象,計(jì)算到gcroot的最短引用路徑,確定是否發(fā)生泄漏。
  val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds)
  return HeapAnalysisSuccess(
      heapDumpFile = heapDumpFile,
     createdAtTimeMillis = System.currentTimeMillis(),
     analysisDurationMillis = since(analysisStartNanoTime),
     metadata = metadata,
     applicationLeaks = applicationLeaks,
     libraryLeaks = libraryLeaks
  )
}

這里leakingObjectFinder.findLeakingObjectIds實(shí)際上是KeyedWeakReferenceFinder,先通過它來獲取泄漏對(duì)象的id集合。然后通過findLeaks針對(duì)這些疑似泄漏的對(duì)象,計(jì)算到gcroot的最短引用路徑,確定是否發(fā)生泄漏。

最后構(gòu)建LeakTrace,傳遞引用鏈,呈現(xiàn)分析結(jié)果。

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

官方說明:

image.png

從1.6.3版本開始,有比較大的變化,簡(jiǎn)單總結(jié)起來:

  • java切到kotlin

  • heap分析庫(kù)從haha轉(zhuǎn)為Shark,haha本身也是square的開源庫(kù):https://github.com/square/haha,Shark沒有作為三方開源庫(kù)獨(dú)立存在,而是leakCanaray的一個(gè)組件,因此新項(xiàng)目總體kotlin代碼量增加了。

  • 對(duì)內(nèi)存泄漏工作流做了優(yōu)化。

四、工作流總結(jié)

這里以Activity為例,簡(jiǎn)單對(duì)leakCanary核心類關(guān)系做下整理,leackCanaray還提供了FragmentDestroyWatcher,這里就不分析了,原理應(yīng)該是一樣的。

leakCanary核心類參與的工作流梳理
  • 應(yīng)用進(jìn)程部分主要是對(duì)Activity/Fragment生命周期監(jiān)控,watcher他們的引用。

  • 內(nèi)存泄漏預(yù)判檢測(cè)機(jī)制:通過WeakReference +ReferenceQueue來判斷對(duì)象是否被系統(tǒng)GC回收,Activity/Fragment引用被包裝為WeakReference,同時(shí)傳入ReferenceQueue。當(dāng)被包裝的Activity/Fragment對(duì)象生命周期結(jié)束,被gc檢測(cè)到,則會(huì)將它添加到 ReferenceQueue 中,等ReferenceQueue處理。當(dāng) GC 過后對(duì)象一直不被加入 ReferenceQueue,它可能存在內(nèi)存泄漏。

  • 是否觸發(fā)dump操作邏輯:這里會(huì)主動(dòng)觸發(fā)一次gc,再來看看是否有沒被回收的弱引用對(duì)象。應(yīng)用在前臺(tái),需要滿足5個(gè)及以上泄漏對(duì)象才觸發(fā)dump操作,后臺(tái)滿足1個(gè)就行,但是前后臺(tái)均還會(huì)收一個(gè)nonpReason的制約,這個(gè)reason相當(dāng)于一個(gè)統(tǒng)一的容錯(cuò),保存判斷l(xiāng)eakCanary是否安裝、配置是否正確、之前的noify通知發(fā)沒發(fā)等等。

  • dump hprof文件通過 Debug.dumpHprofData(filePath)來實(shí)現(xiàn),在data/data/package/files/leakcanary 目錄下,文件大小10幾M到幾十M不等。這個(gè)過程應(yīng)該是耗時(shí)的。

  • hprof文件分析工作交給HeapAnalyserService來處理,它本身在一個(gè)單獨(dú)進(jìn)程中,核心功能通過Shark來完成,內(nèi)存泄漏主要工作:從hprof中獲取泄漏的對(duì)象id集合,這里主要是收集沒有被回收的弱引用,針對(duì)這些疑似泄漏的對(duì)象,計(jì)算到gcroot的最短引用路徑,確認(rèn)是否發(fā)生泄漏。如果確認(rèn)有內(nèi)存泄漏,則會(huì)生成統(tǒng)計(jì)報(bào)表輸出。

參考:leakcanary官方說明文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容