LeakCanary2 源碼分析

最後更新時間

2020.02.15 15時html

背景

在 Android 開發工做中,內存泄露一直是讓人比較頭疼的問題。首先內存泄露並非一個 Java 異常,因此咱們並不能實時感知到它,通常只有等到內存溢出的時候,咱們纔會去排除是否發生了內存泄露問題。而每每致使拋異常的代碼並非內存泄露的兇手,而只是壓死駱駝的最後一根稻草而已,這是第一個問題。第二個問題則是,當咱們想要分析內存問題時,首先須要先 dump 內存快照,一般是以 .hprof 結尾的文件,接着再使用 MAT 等內存分析工具去檢測大內存可疑對象,分析對象到 GC Roots 節點的可達性等等。整個流程相對繁瑣,這時候咱們可能會考慮是否有自動化工具,來幫助咱們去分析那些常見的內存泄露場景呢。java

LeakCanary

LeakCanary 就是爲了解決以上問題而誕生的。2019 年 11 月 的時候,LeakCanary2 正式版發佈,和 LeakCanary1 相比,LeakCanary2 有如下改動:node

  • 徹底使用 Kotlin 重寫
  • 使用新的 Heap 分析工具 Shark,替換到以前的 haha,按官方的說法,內存佔用減小了 10 倍
  • 泄露類型分組

其中,將 Heap 分析模塊做爲一個獨立的模塊,是一個很是不錯的改動。這意味着,能夠基於 Shark 來作不少有意思的事情,好比,用於線上分析或者開發一個"本身"的 LeakCanary。android

總體架構

在分析源碼以前,咱們先看 LeakCanary 的總體結構,這有助於咱們對項目總體設計上有必定理解。LeakCanary2 有如下幾個模塊:c++

  • leakcanary-androidgit

    集成入口模塊,提供 LeakCanary 安裝,公開 API 等能力github

  • leakcanary-android-core數組

    核心模塊緩存

  • leakcanary-android-process架構

    和 leakcanary-android 同樣,區別是會在單獨的進程進行分析

  • leakcanary-android-instrumentation

    用於 Android Test 的模塊

  • leakcanary-object-watcher-android,leakcanary-object-watcher-android-androidx,leakcanary-watcher-android-support-fragments

    對象實例觀察模塊,在 Activity,Fragment 等對象的生命週期中,註冊對指定對象實例的觀察,有 Activity,Fragment,Fragment View,ViewModel 等

  • shark-android

    提供特定於 Android 平臺的分析能力。例如設備的信息,Android 版本,已知的內存泄露問題等

  • shark,shark-test

    hprof 文件解析與分析的入口模塊,還有對應的 Test 模塊

  • shark-graph

    分析堆中對象的關係圖模塊

  • shark-hprof,shark-hprof-test

    解析 hprof 文件模塊,還有對應的 Test 模塊

  • shark-log

    日誌模塊

  • shark-cli

    shark-android 的 cli 版本

集成方式

首先,咱們從集成方式入手,LeakCanary1 的依賴爲:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
}
複製代碼

接着在 Application 中調用 LeakCanary.install() 方法。而 LeakCanary2 集成則要簡單很多,只須要增長如下依賴便可:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}
複製代碼

也就是說 LeakCanary2 實現了自動調用 install() 方法,實現方式可能大部分人都能猜到,就是使用的 ContentProvider,相關代碼位於 leakcanary-object-watcher-android 模塊中的 AppWatcherInstaller.kt 中。

AppWatcherInstaller 繼承 ContentProvider,重寫了 onCreate() 方法,這裏利用的是,註冊在 Manifest 文件中的 ContentProvider,會在應用啓動時,由 ActivityThread 建立並初始化。

override fun onCreate(): Boolean {                              
  val application = context!!.applicationContext as Application 
  InternalAppWatcher.install(application)                       
  return true                                                   
}                                                               
複製代碼

AppWatcherInstaller 有兩個實現類,一個是 MainProcess,當咱們使用 leakcanary-android 模塊時,會默認使用這個,表示在當前 App 進程中使用 LeakCanary。另一個類爲 LeakCanaryProcess,當使用 leakcanary-android-process 模塊代替 leakcanary-android 模塊時,則會使用這個類,咱們能夠看下 leakcanary-android-process 的 Manifest 文件:

這裏利用的 leakcanary-android-process Manifest 優先級要高於 leakcanary-object-watcher-android

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.squareup.leakcanary">

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

    <provider android:name="leakcanary.internal.AppWatcherInstaller$LeakCanaryProcess" android:authorities="${applicationId}.leakcanary-process.installer" android:process=":leakcanary" android:exported="false"/>
  </application>

</manifest>
複製代碼

能夠看到,HeapAnalyzerServiceLeakCanaryProcess 都會在運行在 :leakcanary 進程,關於 HeapAnalyzerService 的做用,咱們後面會講到。

這裏有個須要注意的是,若是使用 LeakCanaryProcess,默認會禁用 Watcher 功能,這個也很好理解,處於不一樣進程,是沒辦法觀察到 APP 進程的對象。

internal class LeakCanaryProcess : AppWatcherInstaller() {      
  override fun onCreate(): Boolean {                            
    super.onCreate()                                            
    AppWatcher.config = AppWatcher.config.copy(enabled = false) 
    return true                                                 
  }                                                             
}                                                               
複製代碼

AppWatcher

Watcher 功能的入口位於 InternalAppWatcher.install() 方法中,這個方法的調用時機則是咱們上面說到的 AppWatcherInstaller.onCreate() 中。

fun install(application: Application) {                                                                 
  ActivityDestroyWatcher.install(application, objectWatcher, configProvider) 
  FragmentDestroyWatcher.install(application, objectWatcher, configProvider) 
  onAppWatcherInstalled(application)                                         
}                                                                            
複製代碼

這裏主要作了兩件事,首先是 Activity 和 Fragment 對象的註冊觀察,這裏咱們以 ActivityDestroyWatcher 爲例,Fragment 的處理也是相似的。

ActivityDestroyWatcher

fun install( application: Application, objectWatcher: ObjectWatcher, configProvider: () -> Config                                                                 
) {                                                                                            
  val activityDestroyWatcher =                                                                 
    ActivityDestroyWatcher(objectWatcher, configProvider)                                      
  application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)    
}                                                                                              
複製代碼

調用 registerActivityLifecycleCallbacks() 方法註冊 Activity 生命週期回調。

override fun onActivityDestroyed(activity: Activity) {                                            
  if (configProvider().watchActivities) {                                                         
    objectWatcher.watch(                                                                          
        activity, "${activity::class.java.name} received Activity#onDestroy() callback"           
    )                                                                                             
  }                                                                                               
}                                                                                                 
複製代碼

在每一個 Activity.onDestory 回調中,將每一個 Activity 對象加到觀察列表中。

private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

private val queue = ReferenceQueue<Any>()

@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)                                           
                                                                                                                                                                                                                                          
  watchedObjects[key] = reference                                                                                           
  checkRetainedExecutor.execute {                                                                                           
    moveToRetained(key)                                                                                                     
  }                                                                                                                         
} 

private fun removeWeaklyReachableObjects() {                                                     
  var ref: KeyedWeakReference?                                                               
  do {                                                                                       
    ref = queue.poll() as KeyedWeakReference?                                                
    if (ref != null) {                                                                       
      watchedObjects.remove(ref.key)                                                         
    }                                                                                        
  } while (ref != null)                                                                      
}                                                                                            
複製代碼

首先咱們要知道 KeyedWeakReference 繼承於 WeakReference,弱引用是不會阻止 GC 回收對象的,同時咱們能夠在構造函數中傳遞一個 ReferenceQueue,用於對象被 GC 後存放的隊列。

class KeyedWeakReference(
  referent: Any,
  val key: String,
  val description: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>
複製代碼

因此 removeWeaklyReachableObjects() 方法的做用就是將已經被 GC 的對象從 watchedObjects 集合中刪除。

當咱們調用 watch() 方法時,先清理已經被 GC 的對象,接着將須要觀察的對象,存儲爲一個 KeyedWeakReference 的弱引用對象,再存放到 watchedObjects 集合中,最後使用 checkRetainedExecutor 安排一次 moveToRetained 任務。

checkRetainedExecutor 是使用 Handler 實現,默認延遲 5s 執行任務。

val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
private val checkRetainedExecutor = Executor {                            
  mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)     
}                                                                        
複製代碼

接下來,咱們再來看下 moveToRetained() 的代碼:

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

一樣的,先調用一次 removeWeaklyReachableObjects() 刪除已經 GC 的對象,那麼剩下的對象就能夠認爲是被保留(沒辦法 GC)的對象,會調通知事件。

onAppWatcherInstalled

InternalAppWatcher.install() 方法的最後,還有一個 onAppWatcherInstalled 的調用,它是一個方法對象,在 Kotlin 中一切皆對象,包括方法,它的賦值在 init 塊中:

init {                                                                                         
  val internalLeakCanary = try {                                                               
    val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")           
    leakCanaryListener.getDeclaredField("INSTANCE")                                            
        .get(null)                                                                             
  } catch (ignored: Throwable) {                                                               
    NoLeakCanary                                                                               
  }                                                                                            
  @kotlin.Suppress("UNCHECKED_CAST")                                                           
  onAppWatcherInstalled = internalLeakCanary as (Application) -> Unit                          
}                                                                                              
複製代碼

這段代碼對不熟悉 Kotlin 的小夥伴來講可能有點繞,首先 internalLeakCanary 是一個方法對象,它的方法簽名轉化爲 Java 代碼爲:

void invoke(Application) {
  
}
複製代碼

而這個對象的值是經過反射獲取的 InternalLeakCanary.INSTANCE 這是一個單例對象。InternalLeakCanary 位於 leakcanary-android-core 模塊,這也是須要反射的緣由,這樣的處理值得商榷。

當調用 onAppWatcherInstalled() 方法時,實際會調用 InternalLeakCanary.invoke() 方法:

override fun invoke(application: Application) {                                          
  this.application = application                                                         
                                                                                         
  AppWatcher.objectWatcher.addOnObjectRetainedListener(this)                             
                                                                                         
  val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)                 
                                                                                         
  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.registerVisibilityListener { applicationVisible ->                         
    this.applicationVisible = applicationVisible                                         
    heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)                   
  }                                                                                      
  registerResumedActivityListener(application)                                           
  addDynamicShortcut(application)                                                        
                                                                                         
  disableDumpHeapInTests()                                                               
}                                                                                        
複製代碼

這裏作的事情比較多,首先是 addOnObjectRetainedListener() 方法,這裏會註冊一個 OnObjectRetainedListener 事件,也就是咱們上面說到的在 moveToRetained() 方法中的回調事件。

AndroidHeapDumper 則是經過調用 Debug.dumpHprofData() 方法從虛擬機中 dump hprof 文件。

GcTrigger 經過調用 Runtime.getRuntime().gc() 方法觸發虛擬機進行 GC 操做。

HeapDumpTrigger 管理觸發 Heap Dump 的邏輯,有兩個地方會觸發 Heap Dump:

  • 保留對象超過闕值

    這個闕值默認爲 5(應用可見的狀況下),能夠經過 Config 配置:

    val retainedVisibleThreshold: Int = 5
    複製代碼

    ObjectWatcher 回調 onObjectRetained() 方法時,HeapDumpTrigger.onObjectRetained() 方法會被調用:

    fun onObjectRetained() {                     
      scheduleRetainedObjectCheck(               
          reason = "found new object retained",  
          rescheduling = false                   
      )                                          
    }
    
    private fun scheduleRetainedObjectCheck( reason: String, rescheduling: Boolean, delayMillis: Long = 0L ) {                                                                                                                  
      val checkCurrentlyScheduledAt = checkScheduledAt                                                                   
      if (checkCurrentlyScheduledAt > 0) {
        // 同時只會有一個任務
        val scheduledIn = checkCurrentlyScheduledAt - SystemClock.uptimeMillis()                                         
        return                                                                                                           
      }                                                                                                               
      checkScheduledAt = SystemClock.uptimeMillis() + delayMillis                                                        
      backgroundHandler.postDelayed({                                                                                    
        checkScheduledAt = 0                                                                                             
        checkRetainedObjects(reason)                                                                                     
      }, delayMillis)                                                                                                    
    }
    
    private fun checkRetainedObjects(reason: String) {                                                                         
      val config = configProvider()                                                                                            
      if (!config.dumpHeap) {                                                                                                  
        return                                                                                                                 
      }                                                                                                                        
                                                                                                                               
      var retainedReferenceCount = objectWatcher.retainedObjectCount                                                           
                                                                                                                               
      if (retainedReferenceCount > 0) {
        // 先執行一次 GC
        gcTrigger.runGc()                                                                                                      
        retainedReferenceCount = objectWatcher.retainedObjectCount                                                             
      }                                                                                                                        
      
      // 檢測當前保留對象數量
      if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return                                  
                                                                                                                               
      if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
        // 默認 debug 時不執行,從新安排到 20s 後
        scheduleRetainedObjectCheck(                                                                                           
            reason = "debugger is attached",                                                                                   
            rescheduling = true,                                                                                               
            delayMillis = WAIT_FOR_DEBUG_MILLIS                                                                                
        )                                                                                                                      
        return                                                                                                                 
      }                                                                                                                        
                                                                                                                               
      val now = SystemClock.uptimeMillis()                                                                                     
      val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis                                                          
      if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {                                                       
         // 60s 內只會執行一次,從新安排 
        scheduleRetainedObjectCheck(                                                                                           
            reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",     
            rescheduling = true,                                                                                               
            delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis                                          
        )                                                                                                                      
        return                                                                                                                 
      }                                                                                                                        
                                                                                                                               
     	                   
      dismissRetainedCountNotification() 
      // 執行 dump heap
      dumpHeap(retainedReferenceCount, retry = true)                                                                           
    }
    
    private fun checkRetainedCount( retainedKeysCount: Int, retainedVisibleThreshold: Int ): Boolean {                                                                                                        
      val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount                                          
      lastDisplayedRetainedObjectCount = retainedKeysCount                                                              
      if (retainedKeysCount == 0) {
        // 沒有保留對象
        return true                                                                                                     
      }                                                                                                                 
                                                                                                                        
      if (retainedKeysCount < retainedVisibleThreshold) { 
        // 低於闕值
        if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {                                            
          // 當前應用可見,或者不可見時間間隔少於 5s,從新安排到 2s 後 
          scheduleRetainedObjectCheck(                                                                                  
              reason = "found only $retainedKeysCount retained objects (< $retainedVisibleThreshold while app visible)",
              rescheduling = true,                                                                                      
              delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS                                                            
          )                                                                                                             
          return true                                                                                                   
        }                                                                                                               
      }                                                                                                                 
      return false                                                                                                      
    }                                                                                                                   
                                                                                                                        
    複製代碼

    上面的代碼稍微有點長,因此在關鍵代碼處添加了註釋。在執行 heap dump 以前,須要處理幾種狀況,好比當前是否是處於調試模式,距離上一次執行有沒有超過 60s,當前應用是否處於可見狀態等等,最終執行的方法是 dumpHeap()

    private fun dumpHeap( retainedReferenceCount: Int, retry: Boolean ) {                                                                                         
      val heapDumpFile = heapDumper.dumpHeap() 
      // 由於這些對象咱們已經 dump 出來分析了,因此不必保留它們了
      objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)                             
      HeapAnalyzerService.runAnalysis(application, heapDumpFile)                                
    }
    
    // ObjectWatcher.kt
    @Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {        
      val weakRefsToRemove =                                                         
        watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis } 
      weakRefsToRemove.values.forEach { it.clear() }                                 
      watchedObjects.keys.removeAll(weakRefsToRemove.keys)                           
    }                                                                                
    複製代碼

    首先調用 HeapDumper.dumpHeap() 獲取 hprof 文件,接着調用 ObjectWatcher.clearObjectsWatchedBefore() 方法清理,最後調用 HeapAnalyzerService.runAnalysis() 進行分析。

小結

ObjectWatcher 保存弱引用對象,再到 HeapDumpTrigger 觸發 heap dump,整個過程是很是清晰的。

HeapAnalyzerService

HeapAnalyzerService 是繼承於 IntentService,調用 runAnalysis() 方法最終會調用到 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()           
  )                                                                            
}                                                                              
複製代碼

proguardMappingReader 是用於處理代碼混淆的,支持在測試版本打開代碼混淆開關,PROGUARD_MAPPING_FILE_NAME 表示 Mapping 文件,這個文件在 leakcanary-deobfuscation-gradle-plugin 模塊處理的,具體的能夠看 CopyObfuscationMappingFileTask

HeapAnalyzer.analyze() 這個方法的做用是:從 hprof 文件中搜索泄露對象,而後計算它們到 GC Roots 的最短路徑。

Hprof.open(heapDumpFile)                                                                    
    .use { hprof ->                                                                         
      val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)                         
      val helpers =                                                                         
        FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)  
      helpers.analyzeGraph(                                                                 
          metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime       
      )                                                                                     
    }                                                                                       
複製代碼

解析 hprof 文件

首先,經過調用 Hprof.open() 讀取 hprof 文件:

fun open(hprofFile: File): Hprof {                                                              
  val fileLength = hprofFile.length()                                                           
  if (fileLength == 0L) {                                                                       
    throw IllegalArgumentException("Hprof file is 0 byte length")                               
  }                                                                                             
  val inputStream = hprofFile.inputStream()                                                     
  val channel = inputStream.channel                                                             
  val source = Okio.buffer(Okio.source(inputStream))                                            
                                                                                                
  val endOfVersionString = source.indexOf(0)                                                    
  val versionName = source.readUtf8(endOfVersionString)                                         
                                                                                                
  val hprofVersion = supportedVersions[versionName]                                             
                                                                                                                                                                                       
  // Skip the 0 at the end of the version string. 
  source.skip(1)                                                                                
  val identifierByteSize = source.readInt()                                                     
                                                                                                
  // heap dump timestamp 
  val heapDumpTimestamp = source.readLong()                                                     
                                                                                                
  val byteReadCount = endOfVersionString + 1 + 4 + 8                                            
                                                                                                
  val reader = HprofReader(source, identifierByteSize, byteReadCount)                           
                                                                                                
  return Hprof(                                                                                 
      channel, source, reader, heapDumpTimestamp, hprofVersion, fileLength                      
  )                                                                                             
}                                                                                               
複製代碼

關於 hprof 文件

hprof 是由 JVM TI Agent HPROF 生成的一種二進制文件,關於 hprof 文件格式能夠看這裏

hprof 文件能夠分爲如下幾個部分:

  • header

    header 中有如下三個部分組成:

    1. 文件格式名和版本號

      JDK 1.6 的值爲 "JAVA PROFILE 1.0.2",在此以前還有 "JAVA PROFILE 1.0" 和 「JAVA PROFILE 1.0.1」,若是是 Android 平臺的話,這個值爲 "JAVA PROFILE 1.0.3",這也是爲何 MAT 不支持直接解析 Android 平臺生成的 hprof 文件了。

    2. identifiers

      4 字節,表示 ID 的大小,它的值可能爲 4 或者 8,表示一個 ID 須要用 4 字節或者 8 字節來表示。

    3. 時間戳

      高位 4 字節 + 低位 4 字節

  • records

    record 表示文件中記錄的信息,每一個 record 都由如下 4 個部分組成:

    1. tag

      1 字節,表示 record 的類型,支持的值能夠看文檔。

    2. time

      4 字節,表示 record 的時間戳。

    3. length

      4 字節,表示 body 的字節長度。

    4. body

      表示 record 中存儲的數據。

JVM TI(JVM tool interface)表示虛擬機工具接口,用於提供查詢和控制虛擬機中運行的程序。

Agent 表示代理程序,用於調用 JVM TI 的,會運行在 JVM 的進程中,通常經過 java -agentlibjava -agentpath 啓動。

在瞭解完 hprof 文件的格式後,咱們再來看 LeakCanary 的解析 hprof 文件的代碼,一樣的,先依次讀取 versionNameidentifierByteSizeheapDumpTimestamp 後,再建立一個 HprofReader 用於讀取 records。HprofReader 的工做方式也是相似的,根據 tag 的值去讀取不一樣的 record,這裏咱們以 "STRING IN UTF8" 爲例:

when (tag) {                                             
   STRING_IN_UTF8 -> {                                    
     if (readStringRecord) {                              
       val recordPosition = position                      
       val id = readId()                                  
       val stringLength = length - identifierByteSize     
       val string = readUtf8(stringLength)                
       val record = StringRecord(id, string)              
       listener.onHprofRecord(recordPosition, record)     
     } else {                                             
       skip(length)                                       
     }                                                    
   }
 }
複製代碼

生成 heap graph

HeapGraph 用於表示 heap 中的對象關係圖,經過調用 HprofHeapGraph.indexHprof() 生成:

fun indexHprof( hprof: Hprof, proguardMapping: ProguardMapping? = null, indexedGcRootTypes: Set<KClass<out GcRoot>> = setOf( JniGlobal::class, JavaFrame::class, JniLocal::class, MonitorUsed::class, NativeStack::class, StickyClass::class, ThreadBlock::class, ThreadObject::class, JniMonitor::class )                                                                                                                     
): HeapGraph {                                                                                                          
  val index = HprofInMemoryIndex.createReadingHprof(hprof, proguardMapping, indexedGcRootTypes)                         
  return HprofHeapGraph(hprof, index)                                                                                   
}                                                                                                                       
複製代碼

indexedGcRootTypes 表示咱們要收集的 GC Roots 節點,能夠做爲 GC Roots 節點的有如下對象:

// Traditional.
  HPROF_ROOT_UNKNOWN = 0xFF,
  // native 中的全局變量
  HPROF_ROOT_JNI_GLOBAL = 0x01,
  // native 中的局部變量
  HPROF_ROOT_JNI_LOCAL = 0x02,
  // java 中的局部變量
  HPROF_ROOT_JAVA_FRAME = 0x03,
  // native 中的入參和出參
  HPROF_ROOT_NATIVE_STACK = 0x04,
  // 系統類
  HPROF_ROOT_STICKY_CLASS = 0x05,
  // 活動線程引用的對象
  HPROF_ROOT_THREAD_BLOCK = 0x06,
  // 調用 wait() 或者 notify(),或者 synchronized 的對象
  HPROF_ROOT_MONITOR_USED = 0x07,
  // 活動線程
  HPROF_ROOT_THREAD_OBJECT = 0x08,

  // Android.
  // 調用 String.intern() 的對象
  HPROF_ROOT_INTERNED_STRING = 0x89,
  // 等待 finalizer 調用的對象
  HPROF_ROOT_FINALIZING = 0x8a,  // Obsolete.
  // 用於鏈接 debugger 的對象
  HPROF_ROOT_DEBUGGER = 0x8b,
  // 未知
  HPROF_ROOT_REFERENCE_CLEANUP = 0x8c,  // Obsolete.
  // 未知
  HPROF_ROOT_VM_INTERNAL = 0x8d,
  // 未知
  HPROF_ROOT_JNI_MONITOR = 0x8e,
  // 不可達,但不是 GC Root
  HPROF_UNREACHABLE = 0x90,  // Obsolete.
複製代碼

上面的是 JVM 定義的,下面的是 Android 平臺特有的,具體能夠看 hprof.cc

雖然存在很多 GC Roots 節點,但 LeakCanary 只選取了部分:

  • HPROF_ROOT_JNI_GLOBAL

    native 中的全局變量

  • HPROF_ROOT_JAVA_FRAME

    java 中的局部變量

  • HPROF_ROOT_JNI_LOCAL

    native 中的局部變量

  • HPROF_ROOT_MONITOR_USED

    調用 wait() 或者 notify(),或者 synchronized 的對象

  • HPROF_ROOT_NATIVE_STACK

    native 中的入參和出參

  • HPROF_ROOT_STICKY_CLASS

    系統類

  • HPROF_ROOT_THREAD_BLOCK

    活動線程引用的對象

  • HPROF_ROOT_THREAD_OBJECT

    活動線程

  • HPROF_ROOT_JNI_MONITOR

    未知,多是 native 中的同步對象

接着會從 hprof 文件中讀取 records,讀取原理能夠參考 hprof 文件格式。這裏有個小細節,LeakCanary 只會讀取如下幾種類型的 record:

  • STRING IN UTF8

    0x01,UTF8 格式的字符串

  • LOAD CLASS

    0x02,虛擬機中加載的類

  • HEAP DUMP 中的 CLASS DUMP

    0x0C 和 0x20,dump 出來內存中的類實例

    hprof 1.0.2 版本會用 HEAP DUMP SEGMENT 0x1C 做用是同樣的

  • HEAP DUMP 中的 INSTANCE DUMP

    0x0C 和 0x21,dump 出來內存中的對象實例

  • HEAP DUMP 中的 OBJECT ARRAY DUMP

    0x0C 和 0x22,dump 出來內存中的對象數組實例

  • HEAP DUMP 中的 PRIMITIVE ARRAY DUMP

    0x0C 和 0x23,dump 出來內存中的原始類型數組實例

  • HEAP 中 GC Roots

    這裏包括了上面定義的全部 GC Roots 對象實例

查詢泄露對象

在生成 heap graph 後,咱們就能夠根據它,來獲取泄露對象的 objectIds:

// FindLeakInput.analyzeGraph()
val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
複製代碼

LeakingObjectFinder 用於查詢泄露對象,它的實現有兩個:KeyedWeakReferenceFinderFilteringLeakingObjectFinder,默認爲 KeyedWeakReferenceFinder,即經過 KeyedWeakReference 引用的對象,關於 KeyedWeakReference 的做用咱們在 AppWatcher 那裏有說到。

internal fun findKeyedWeakReferences(graph: HeapGraph): List<KeyedWeakReferenceMirror> {                            
  return graph.context.getOrPut(KEYED_WEAK_REFERENCE.name) {                                                        
    val addedToContext: List<KeyedWeakReferenceMirror> = graph.instances                                            
        .filter { instance ->                                                                                       
          val className = instance.instanceClassName                                                                
          className == "leakcanary.KeyedWeakReference" || className == "com.squareup.leakcanary.KeyedWeakReference" 
        }                                                                                                           
        .map {                                                                                                       
          KeyedWeakReferenceMirror.fromInstance(                                                                    
              it, heapDumpUptimeMillis                                                                              
          )                                                                                                         
        }                                                                                                           
        .filter { it.hasReferent }                                                                                  
        .toList()                                                                                                   
    graph.context[KEYED_WEAK_REFERENCE.name] = addedToContext                                                       
    addedToContext                                                                                                  
  }                                                                                                                 
}                                                                                                                   
複製代碼

KeyedWeakReferenceFinder 經過過濾 heap dump 中的全部 KeyedWeakReference 實例,來獲取泄露對象實例。

FilteringLeakingObjectFinder 則是用於咱們自定義的泄露對象判斷邏輯:

override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> {       
  return graph.objects                                                 
      .filter { heapObject ->                                          
        filters.any { filter ->                                        
          filter.isLeakingObject(heapObject)                           
        }                                                              
      }                                                                
      .map { it.objectId }                                             
      .toSet()                                                         
}                                                                      
複製代碼

生成泄露對象報告

LeakCanary 定義了兩個泄露類型:ApplicationLeakLibraryLeak

  • ApplicationLeak

    表示應用自己致使內存泄露

  • LibraryLeak

    表示依賴庫致使的內存泄露,例如 Android Framework 等

以上兩種泄露都是經過調用 FindLeakInput.findLeaks() 方法來獲取的:

private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): Pair<List<ApplicationLeak>, List<LibraryLeak>> { 
  val pathFinder = PathFinder(graph, listener, referenceMatchers)                                                  
  val pathFindingResults =                                                                                         
    pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)                                     
  return buildLeakTraces(pathFindingResults)                                                                       
}                                                                                                                  
複製代碼
查詢泄露對象到 GC Roots 的路徑

這是經過 PathFinder.findPathsFromGcRoots() 方法實現的:

fun findPathsFromGcRoots( leakingObjectIds: Set<Long>, computeRetainedHeapSize: Boolean ): PathFindingResults {                                                                
                                                                                       
  val sizeOfObjectInstances = determineSizeOfObjectInstances(graph)                    
                                                                                       
  val state = State(leakingObjectIds, sizeOfObjectInstances, computeRetainedHeapSize)  
                                                                                       
  return state.findPathsFromGcRoots()                                                  
} 

private fun State.findPathsFromGcRoots(): PathFindingResults {                                           
  enqueueGcRoots()                                                                                       
                                                                                           // 省略 
  return PathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)                           
}                                                                                                        
複製代碼

State.findPathsFromGcRoots() 的代碼有點長,咱們一點點分析。

首先是 enqueueGcRoots() 方法,它的做用是將全部 GC Roots 節點放入到隊列中:

private fun State.enqueueGcRoots() {
  // 將 GC Roots 進行排序
  // 排序是爲了確保 ThreadObject 在 JavaFrames 以前被訪問,這樣能夠經過 ThreadObject.threadsBySerialNumber 獲取它的線程信息
  val gcRoots = sortedGcRoots()                                                           
  // 存儲線程名稱
  val threadNames = mutableMapOf<HeapInstance, String>()                                   
  // 存儲線程的 SerialNumber,能夠經過 SerialNumber 訪問對應的線程信息
  val threadsBySerialNumber = mutableMapOf<Int, Pair<HeapInstance, ThreadObject>>()              
  gcRoots.forEach { (objectRecord, gcRoot) ->                                                    
    if (computeRetainedHeapSize) {
      // 計算泄露對象而保留的內存大小
      undominateWithSkips(gcRoot.id)                                                             
    }                                                                                            
    when (gcRoot) {                                                                              
      is ThreadObject -> {
        // 活動的 Thread 實例
        // 緩存 threadsBySerialNumber
        threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot 
        // 入列 NormalRootNode
        enqueue(NormalRootNode(gcRoot.id, gcRoot))                                               
      }                                                                                          
      is JavaFrame -> {                                                             
        // Java 局部變量 
        val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]                        
        if (threadPair == null) {                                                                
          // Could not find the thread that this java frame is for. 
          enqueue(NormalRootNode(gcRoot.id, gcRoot))                                             
        } else {                                                                                 
                                                                                                 
          val (threadInstance, threadRoot) = threadPair                                          
          val threadName = threadNames[threadInstance] ?: {                                      
            val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""    
            threadNames[threadInstance] = name                                                   
            name                                                                                 
          }()                                                                           
          
          // RefreshceMatchers 用於匹配已知的引用節點
          // IgnoredReferenceMatcher 表示忽略這個引用節點
          // LibraryLeakReferenceMatcher 表示這是庫內存泄露對象
          val referenceMatcher = threadNameReferenceMatchers[threadName]                         
                                                                                                 
          if (referenceMatcher !is IgnoredReferenceMatcher) {                                    
            val rootNode = NormalRootNode(threadRoot.id, gcRoot)                                 
                                                                                                 
            val refFromParentType = LOCAL                                                        
            val refFromParentName = ""                                                           
                                                                                                 
            val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {               
              LibraryLeakChildNode(                                                              
                  objectId = gcRoot.id,                                                          
                  parent = rootNode,                                                             
                  refFromParentType = refFromParentType,                                         
                  refFromParentName = refFromParentName,                                         
                  matcher = referenceMatcher                                                     
              )                                                                                  
            } else {                                                                             
              NormalNode(                                                                        
                  objectId = gcRoot.id,                                                          
                  parent = rootNode,                                                             
                  refFromParentType = refFromParentType,                                         
                  refFromParentName = refFromParentName                                          
              )                                                                                  
            }                                                                           
            // 入列 LibraryLeakChildNode 或 NormalNode 
            enqueue(childNode)                                                                   
          }                                                                                      
        }                                                                                        
      }                                                                                          
      is JniGlobal -> {                                                               
       // Native 全局變量
        // 是否匹配已知引用節點
        val referenceMatcher = when (objectRecord) {                                             
          is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]                          
          is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]          
          is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]          
          is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]       
        }                                                                                        
        if (referenceMatcher !is IgnoredReferenceMatcher) {                                      
          if (referenceMatcher is LibraryLeakReferenceMatcher) {                 
          // 入列 LibraryLeakRootNode 
            enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))                    
          } else {                                                                    
          // 入列 NormalRootNode 
            enqueue(NormalRootNode(gcRoot.id, gcRoot))                                           
          }                                                                                      
        }                                                                                        
      }                                                                                 
      // 其餘 GC Roots,入列 NormalRootNode 
      else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))                                         
    }                                                                                            
  }                                                                                              
}                                                                                                
複製代碼

在將 GC Roots 節點入列的過程,有兩個地方值得注意:

  1. ReferenceMatcher

    ReferenceMatcher 用於匹配引用節點,判斷是否要忽略它。LeakCanary 支持 4 種類型的匹配:

    • 類實例字段

      緩存在 fieldNameByClassName 裏,例如,android.os.Message 中的 obj 字段

    • 類靜態字段

      緩存在 staticFieldNameByClassName 裏,例如,android.app.ActivityManagermContext 字段

    • 指定線程

      緩存在 threadNames 裏,例如,FinalizerWatchdogDaemon 線程

    • Native 全局變量

      緩存在 jniGlobals 裏,例如,android.widget.Toast\$TN

    內置的引用節點匹配爲 AndroidReferenceMatchers.appDefaults

  2. VisitQueue

    PathFinder 中有兩個隊列,一個優先級更高的 toVisitQueue,另一個是 toVisitLastQueue,同時提供 toVisitSettoVisitLastSet 用於提供常數級查詢。

    隊列中的節點分爲兩種:

    • RootNode

      根節點,它有兩個實現類:

      • LibraryLeakRootNode

        依賴庫的泄露根節點

      • NormalRootNode

        普通的根節點

    • ChildNode

      子節點,能夠經過 parent 字段訪問父節點。它有兩個實現類:

      • LibraryLeakChildNode

        依賴庫的泄露子節點

      • NormalNode

        普通的字節點

    如下 3 種狀況會將節點放入到 toVisitLastQueue 中:

    • LibraryLeakNode
    • GC Root 爲 ThreadObject
    • 父節點的 GC Root 爲 JavaFrame

    由於這 3 種致使的內存泄露狀況比較少,因此下降它們的訪問優先級。

    val visitLast =                                                                                
      node is LibraryLeakNode ||                                                                   
          // We deprioritize thread objects because on Lollipop the thread local values are stored 
          // as a field. 
          (node is RootNode && node.gcRoot is ThreadObject) ||                                     
          (node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)       
    複製代碼

在將全部的 GC Roots 節點入列後,使用廣度優先遍歷全部的節點,當訪問節點是泄露節點,則添加到 shortestPathsToLeakingObjects 中:

val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()                                   
visitingQueue@ while (queuesNotEmpty) {                                                                  
  val node = poll()                                                                                      
                                                                                                         
  if (checkSeen(node)) {                                                                                 
    throw IllegalStateException(                                                                         
        "Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"   
    )                                                                                                    
  }                                                                                                      
                                                                                                         
  if (node.objectId in leakingObjectIds) {                                                               
    shortestPathsToLeakingObjects.add(node)                                                              
    // Found all refs, stop searching (unless computing retained size) 
    if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {                                   
      if (computeRetainedHeapSize) {                                                                     
        listener.onAnalysisProgress(FINDING_DOMINATORS)                                                  
      } else {                                                                                           
        break@visitingQueue                                                                              
      }                                                                                                  
    }                                                                                                    
  }                                                                                                      
                                                                                                         
  when (val heapObject = graph.findObjectById(node.objectId)) {                                          
    is HeapClass -> visitClassRecord(heapObject, node)                                                   
    is HeapInstance -> visitInstance(heapObject, node)                                                   
    is HeapObjectArray -> visitObjectArray(heapObject, node)                                             
  }                                                                                                      
}                                                                                                        
複製代碼

在遍歷子節點時,有 3 種狀況須要考慮:

  1. HeapClass

    當節點表示 HeapClass,咱們將它的靜態變量入列:

    val node = when (val referenceMatcher = ignoredStaticFields[fieldName]) {   
      null -> NormalNode(                                                       
          objectId = objectId,                                                  
          parent = parent,                                                      
          refFromParentType = STATIC_FIELD,                                     
          refFromParentName = fieldName                                         
      )                                                                         
      is LibraryLeakReferenceMatcher -> LibraryLeakChildNode(                   
          objectId = objectId,                                                  
          parent = parent,                                                      
          refFromParentType = STATIC_FIELD,                                     
          refFromParentName = fieldName,                                        
          matcher = referenceMatcher                                            
      )
      // 忽略 IgnoredReferenceMatcher
      is IgnoredReferenceMatcher -> null                                        
    }                                                                           
    if (node != null) {                                                         
      enqueue(node)                                                             
    }                                                                           
    複製代碼
  2. HeapInstance

    當節點表示 HeapInstance,咱們將它的實例變量入列:

    val fieldNamesAndValues = instance.readFields()                                   
        .filter { it.value.isNonNullReference }                                       
        .toMutableList()                                                              
                                                                                      
    fieldNamesAndValues.sortBy { it.name }                                            
                                                                                      
    fieldNamesAndValues.forEach { field ->                                            
      val objectId = field.value.asObjectId!!                                         
      if (computeRetainedHeapSize) {                                                  
        updateDominatorWithSkips(parent.objectId, objectId)                           
      }                                                                               
                                                                                      
      val node = when (val referenceMatcher = fieldReferenceMatchers[field.name]) {   
        null -> NormalNode(                                                           
            objectId = objectId,                                                      
            parent = parent,                                                          
            refFromParentType = INSTANCE_FIELD,                                       
            refFromParentName = field.name                                            
        )                                                                             
        is LibraryLeakReferenceMatcher ->                                             
          LibraryLeakChildNode(                                                       
              objectId = objectId,                                                    
              parent = parent,                                                        
              refFromParentType = INSTANCE_FIELD,                                     
              refFromParentName = field.name,                                         
              matcher = referenceMatcher                                              
          )
        // 忽略 IgnoredReferenceMatcher
        is IgnoredReferenceMatcher -> null                                            
      }                                                                               
      if (node != null) {                                                             
        enqueue(node)                                                                 
      }                                                                               
    }                                                                                 
    複製代碼
  3. HeapObjectArray

    當節點表示 HeapObjectArray,咱們將它的非空元素入列:

    val nonNullElementIds = record.elementIds.filter { objectId ->            
      objectId != ValueHolder.NULL_REFERENCE && graph.objectExists(objectId)  
    }                                                                         
    nonNullElementIds.forEachIndexed { index, elementId ->                    
      if (computeRetainedHeapSize) {                                          
        updateDominatorWithSkips(parent.objectId, elementId)                  
      }                                                                       
      val name = index.toString()                                             
      enqueue(                                                                
          NormalNode(                                                         
              objectId = elementId,                                           
              parent = parent,                                                
              refFromParentType = ARRAY_ENTRY,                                
              refFromParentName = name                                        
          )                                                                   
      )                                                                       
    }                                                                         
    複製代碼

這裏不須要考慮 HeapPrimitiveArray 的狀況,由於原始類型不能致使內存泄露。

至此,咱們經過調用 findPathsFromGcRoots() 方法將全部泄露對象的引用節點都查詢出來了。

最短路徑

在經過 findPathsFromGcRoots() 獲取的節點中,一個泄露對象可能會有多個引用路徑,因此咱們還須要作一次遍歷,找到每一個泄露對象的最短路徑(致使泄露的可能性最大)。

private fun deduplicateShortestPaths(inputPathResults: List<ReferencePathNode>): List<ReferencePathNode> {  
  val rootTrieNode = ParentNode(0)                                                                          
                                                                                                            
  for (pathNode in inputPathResults) {                                                                      
    // Go through the linked list of nodes and build the reverse list of instances from 
    // root to leaking. 
    val path = mutableListOf<Long>()                                                                        
    var leakNode: ReferencePathNode = pathNode                                                              
    while (leakNode is ChildNode) {                                             
    	// 從父節點 -> 子節點 
      path.add(0, leakNode.objectId)                                                                        
      leakNode = leakNode.parent                                                                            
    }                                                                                                       
    path.add(0, leakNode.objectId)                                                                 

    // 這裏的做用是構建樹 
    updateTrie(pathNode, path, 0, rootTrieNode)                                                             
  }                                                                                                         
                                                                                                            
  val outputPathResults = mutableListOf<ReferencePathNode>()                                                
  findResultsInTrie(rootTrieNode, outputPathResults)                                                        
  return outputPathResults                                                                                  
}                                                                             

private fun updateTrie( pathNode: ReferencePathNode, path: List<Long>, pathIndex: Int, parentNode: ParentNode ) {                                                                    
  val objectId = path[pathIndex]                                       
  if (pathIndex == path.lastIndex) { 
    // 當前已是葉子節點 
    // 替換已存在的節點,當前路徑更短 
    parentNode.children[objectId] = LeafNode(objectId, pathNode)       
  } else {                                                             
    val childNode = parentNode.children[objectId] ?: {                 
      val newChildNode = ParentNode(objectId)                          
      parentNode.children[objectId] = newChildNode                     
      newChildNode                                                     
    }()                                                                
    if (childNode is ParentNode) {
      // 遞歸更新 
      updateTrie(pathNode, path, pathIndex + 1, childNode)             
    }                                                                  
  }                                                                    
}                                                                      
複製代碼

經過遍歷泄露對象節點的父節點,構建出一棵樹,多個相同泄露對象節點的不一樣路徑,最終獲取最短路徑的樹。多條最短路徑(不一樣泄露對象)最終合併成一棵樹。

path
上圖中,Leaf 節點就是泄露對象節點,從 GC 節點到 Leaf 節點就是當前泄露對象最短路徑。

生成 LeakTrace

從上面生成泄露對象路徑 ReferencePathNode 到最終的 LeakTrace,這裏只是又作了一層包裝,好比經過 HeapGraph.findObjectById()objectId 轉成對應的 HeapObject

var node: ReferencePathNode = retainedObjectNode                       
  while (node is ChildNode) {                                            
    shortestChildPath.add(0, node)                                       
    pathHeapObjects.add(0, graph.findObjectById(node.objectId))          
    node = node.parent                                                   
  }                                                                      
  val rootNode = node as RootNode                                        
  pathHeapObjects.add(0, graph.findObjectById(rootNode.objectId))        
複製代碼

有兩個地方須要注意下:

  1. ObjectInspector

    經過調用 ObjectInspector.inspect(),能夠對每一個 ObjectReporter 添加一些說明。例如,判斷 Activity 對象是否泄露:

    override fun inspect( reporter: ObjectReporter ) {                                                                                 
        reporter.whenInstanceOf("android.app.Activity") { instance    ->                     
                                  
        val field = instance["android.app.Activity",  "mDestroyed"]                      
                                                                                    
        if (field != null)    {                                                            
        if (field.value.asBoolean!!) {                                                
          leakingReasons += field describedWithValue "true"                           
        } else {                                                                      
        notLeakingReasons += field describedWithValue "false"                       
        }                                                                             
        }                                                                               
      }                                                                                 
    }                                                                      
    複製代碼

    咱們也能夠經過 Config.objectInspectors 添加自定義的 ObjectInspector

  2. LeakingStatus

    經過調用 HeapAnalyzer.computeLeakStatuses() 來計算路徑上每一個節點的泄露狀態:

    private fun computeLeakStatuses(leakReporters: List<ObjectReporter>): List<Pair<LeakingStatus, String>> {
      val lastElementIndex = leakReporters.size - 1                                                          
                                                                                                             
      var lastNotLeakingElementIndex = -1                                                                    
      var firstLeakingElementIndex = lastElementIndex                                                        
                                                                                                             
      val leakStatuses = ArrayList<Pair<LeakingStatus, String>>()                                            
                                                                                                             
      for ((index, reporter) in leakReporters.withIndex()) {                                                 
        // 經過判斷是否存在 leakingReasons 來判斷是否爲泄露節點
        val resolvedStatusPair =                                                                             
          resolveStatus(reporter, leakingWins = index == lastElementIndex).let { statusPair ->               
            if (index == lastElementIndex) {                                                                                                             
              // 葉子節點確定爲泄露狀態 
              when (statusPair.first) {                                                                      
                LEAKING -> statusPair                                                                        
                UNKNOWN -> LEAKING to "This is the leaking object"                                           
                NOT_LEAKING -> LEAKING to "This is the leaking object. Conflicts with ${statusPair.second}"  
              }                                                                                              
            } else statusPair                                                                                
          }                                                                                                  
                                                                                                             
        leakStatuses.add(resolvedStatusPair)                                                                 
        val (leakStatus, _) = resolvedStatusPair                                        
    
        // firstLeakingElementIndex 第一個泄露節點的下標
        // lastNotLeakingElementIndex 最後一個非泄露節點的下標 
        if (leakStatus == NOT_LEAKING) {                                                                     
          lastNotLeakingElementIndex = index                                                                 
          // Reset firstLeakingElementIndex so that we never have 
          // firstLeakingElementIndex < lastNotLeakingElementIndex 
          firstLeakingElementIndex = lastElementIndex                                                        
        } else if (leakStatus == LEAKING && firstLeakingElementIndex == lastElementIndex) {                  
          firstLeakingElementIndex = index                                                                   
        }                                                                                                    
      }                                                                                                      
                                                                                                             
      val simpleClassNames = leakReporters.map { reporter ->                                                 
        recordClassName(reporter.heapObject).lastSegment('.')                                                
      }                                                                                                      
      // lastNotLeakingElementIndex 以前節點不會是泄露狀態 
      for (i in 0 until lastNotLeakingElementIndex) {                                                        
        val (leakStatus, leakStatusReason) = leakStatuses[i]                                                 
        val nextNotLeakingIndex = generateSequence(i + 1) { index ->                                         
          if (index < lastNotLeakingElementIndex) index + 1 else null                                        
        }.first { index ->                                                                                   
          leakStatuses[index].first == NOT_LEAKING                                                           
        }                                                                                                    
                                                                                                             
        // Element is forced to NOT_LEAKING 
        val nextNotLeakingName = simpleClassNames[nextNotLeakingIndex]                                       
        leakStatuses[i] = when (leakStatus) {                                                                
          UNKNOWN -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking"                                    
          NOT_LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking and $leakStatusReason"          
          LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking. Conflicts with $leakStatusReason"  
        }                                                                                                    
      }                                                                                                      
      // firstLeakingElementIndex 以後的節點爲泄露狀態 
      if (firstLeakingElementIndex < lastElementIndex - 1) {                                                 
        // We already know the status of firstLeakingElementIndex and lastElementIndex 
        for (i in lastElementIndex - 1 downTo firstLeakingElementIndex + 1) {                                
          val (leakStatus, leakStatusReason) = leakStatuses[i]                                               
          val previousLeakingIndex = generateSequence(i - 1) { index ->                                      
            if (index > firstLeakingElementIndex) index - 1 else null                                        
          }.first { index ->                                                                                 
            leakStatuses[index].first == LEAKING                                                             
          }                                                                                                  
                                                                                                             
          // Element is forced to LEAKING 
          val previousLeakingName = simpleClassNames[previousLeakingIndex]                                   
          leakStatuses[i] = when (leakStatus) {                                                              
            UNKNOWN -> LEAKING to "$previousLeakingName↑ is leaking"                                         
            LEAKING -> LEAKING to "$previousLeakingName↑ is leaking and $leakStatusReason"                   
            NOT_LEAKING -> throw IllegalStateException("Should never happen")                                
          }                                                                                                  
        }                                                                                                    
      }                                                                                                      
      return leakStatuses                                                                                    
    }                                                                                                        
    複製代碼

小結

至此,咱們已經把 HeapAnalyzerService.analyzeHeap() 方法分析完了,下面咱們用時序圖把這個調用關係再加深下印象:

順序圖

UI 展示

在默認實現的 DefaultOnHeapAnalyzedListener 中,當前 hprof 文件分析成功後,會回調 onHeapAnalyzed() 方法:

override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
  // 入庫 
  val id = LeaksDbHelper(application).writableDatabase.use { db ->                                
    HeapAnalysisTable.insert(db, heapAnalysis)                                                    
  }                                                                                               
                                                                                                  
  val (contentTitle, screenToShow) = when (heapAnalysis) {                                        
    is HeapAnalysisFailure -> application.getString(                                              
        R.string.leak_canary_analysis_failed                                                      
    ) to HeapAnalysisFailureScreen(id)                                                            
    is HeapAnalysisSuccess -> {                                                                   
      val retainedObjectCount = heapAnalysis.allLeaks.sumBy { it.leakTraces.size }                
      val leakTypeCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size     
      application.getString(                                                                      
          R.string.leak_canary_analysis_success_notification, retainedObjectCount, leakTypeCount  
      ) to HeapDumpScreen(id)                                                                     
    }                                                                                             
  }                                                                                               
                                                                                                  
  if (InternalLeakCanary.formFactor == TV) {                                                      
    showToast(heapAnalysis)                                                                       
    printIntentInfo()                                                                             
  } else {                                                                   
    // 顯示通知欄消息 
    showNotification(screenToShow, contentTitle)                                                  
  }                                                                                               
}                                                                                                 
複製代碼

當點擊通知欄消息後,再跳轉到 LeakActivity

val pendingIntent = LeakActivity.createPendingIntent(         
    application, arrayListOf(HeapDumpsScreen(), screenToShow) 
)                                                             
複製代碼

總結

從源碼把 LeakCanary 的核心流程分析下來,能夠看到整個項目中,不論是模塊的劃分,代碼的風格都是很是清晰,特別是用了 kotlin 重寫後,具有了不少 Java 沒有的語法糖,讓代碼的篇幅也很是精簡。總的來講,這個是一個很是不錯的學習項目。

相關文章
相關標籤/搜索