Android 記憶體洩漏檢測工具 LeakCanary(Kotlin版)的實現原理

huansky發表於2021-12-11

LeakCanary 是一個簡單方便的記憶體洩漏檢測框架,做 android 的同學基本都收到過 LeakCanary 檢測出來的記憶體洩漏。目前 LeakCanary 最新版本為 2.7 版本,並且採用 kotlin 重寫了一遍。最近也是對 kotlin 有了一些瞭解後,才敢來分析 LeakCanary 的實現原理。

1. 準備知識

1.1 Reference

Java中的四種引用型別,我們先簡單複習下

  • 強引用,物件有強引用時不能被回收

  • 軟引用 SoftReference,物件只有軟引用時,在記憶體不足時觸發GC會回收該物件

  • 弱引用 WeakReference,物件只有弱引用時,下次GC就會回收該物件

  • 虛引用 PhantomReference,平常很少會用到,原始碼註釋主要用來監聽物件清理前的動作,比Java finalization更靈活,PhantomReference 需要與 ReferenceQueue 一起配合使用。

Reference 主要是負責記憶體的一個狀態,當然它還和java虛擬機器,垃圾回收器打交道。Reference 類首先把記憶體分為4種狀態 Active,Pending,Enqueued,Inactive。

  • Active 一般來說記憶體一開始被分配的狀態都是 Active,

  • Pending 大概是指快要被放進佇列的物件,也就是馬上要回收的物件,

  • Enqueued 就是物件的記憶體已經被回收了,我們已經把這個物件放入到一個佇列中,方便以後我們查詢某個物件是否被回收,

  • Inactive 就是最終的狀態,不能再變為其它狀態。

1.2 ReferenceQueue

引用佇列,當檢測到物件的可到達性更改時,垃圾回收器將已註冊的引用物件新增到佇列中,ReferenceQueue實現了入隊(enqueue)和出隊(poll),還有remove操作,內部元素head就是泛型的Reference。

1.3 簡單例子

當我們想檢測一個物件是否被回收了,那麼我們就可以採用 Reference + ReferenceQueue,大概需要幾個步驟:

  1. 建立一個引用佇列 queue

  2. 建立 Reference 物件,並關聯引用佇列 queue

  3. 在 reference 被回收的時候,Reference 會被新增到 queue 中

建立一個引用佇列  
ReferenceQueue queue = new ReferenceQueue();  
  
// 建立弱引用,此時狀態為Active,並且Reference.pending為空,當前Reference.queue = 上面建立的queue,並且next=null  
WeakReference reference = new WeakReference(new Object(), queue);  
System.out.println(reference);  
// 當GC執行後,由於是弱引用,所以回收該object物件,並且置於pending上,此時reference的狀態為PENDING  
System.gc();  
  
/* ReferenceHandler從pending中取下該元素,並且將該元素放入到queue中,此時Reference狀態為ENQUEUED,Reference.queue = ReferenceENQUEUED */  
  
/* 當從queue裡面取出該元素,則變為INACTIVE,Reference.queue = Reference.NULL */  
Reference reference1 = queue.remove();  
System.out.println(reference1);

那這個可以用來幹什麼了?

可以用來檢測記憶體洩露, github 上面 的 leekCanary 就是採用這種原理來檢測的。

  • 監聽 Activity 的生命週期

  • 在 onDestroy 的時候,建立相應的 Reference 和 ReferenceQueue,並啟動後臺程式去檢測

  • 一段時間之後,從 ReferenceQueue 讀取,若讀取不到相應 activity 的 Reference,有可能發生洩露了,這個時候,再促發 gc,一段時間之後,再去讀取,若在從 ReferenceQueue 還是讀取不到相應 activity 的 Reference,可以斷定是發生記憶體洩露了

  • 發生記憶體洩露之後,dump,分析 hprof 檔案,找到洩露路徑

 那麼是怎麼被新增到佇列裡面去的呢?

Reference 類中有一個特殊的執行緒叫 ReferenceHandler,專門處理那些 pending 連結串列中的引用物件。ReferenceHandler 類是 Reference 類的一個靜態內部類,繼承自 Thread,所以這條執行緒就叫它 ReferenceHandler 執行緒。其中的 run 方法最終會呼叫 tryHandlePending 方法,具體如下:

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                // 使用 'instanceof' 有時會導致OOM
                // 所以在將r從連結串列中摘除時先進行這個操作
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // 移除頭結點,將pending指向其後一個節點
                pending = r.discovered;
                //從連結串列中移除
                r.discovered = null;
            } else {
                // 在鎖上等待可能會造成OOM,因為它會試圖分配exception物件
                if (waitForNotify) {
                    // 導致當前執行緒等待,直到另一個執行緒呼叫此物件的notify()方法或notifyAll()方法,或指定的時間已過
                    lock.wait();
                }
                // 重試
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        // 重試
        return true;
    } catch (InterruptedException x) {
        // 重試
        return true;
    }
 
    // 如果移除的元素是Cleaner型別,則執行其clean方法
    if (c != null) {
        c.clean();
        return true;
    }
 
    ReferenceQueue<? super Object> q = r.queue;
    //對Pending狀態的例項入隊操作
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

可以發現在回收的時候,會把當前的弱引用放到對應的弱引用的佇列中,這和前面的例子是吻合的。具體可以閱讀這篇文章 Java 學習:Reference 和 ReferenceQueue 類

2. LeakCanary使用簡介

在 app 的 build.gradle 中加入依賴

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

LeakCanary 會自動監控 Activity、Fragment、Fragment View、RootView、Service 的洩漏。

如果需要監控其它物件的洩露,可以手動新增如下程式碼:

AppWatcher.objectWatcher.watch(myView, "View was detached")

3. LeakCanary檢測記憶體洩漏的基本流程

3.1 檢測流程

在介紹 LeakCanary 程式碼細節前,先看下檢測的基本流程,避免迷失在繁雜的細節中。總體流程圖如下所示:

  1. ObjectWatcher 建立了一個 KeyedWeakReference 來監視物件.

  2. 稍後,在後臺執行緒中,延時檢查引用是否已被清除,如果沒有則觸發 GC

  3. 如果引用一直沒有被清除,它會dumps the heap 到一個.hprof 檔案中,然後將.hprof 檔案儲存到檔案系統。

  4. 分析過程主要在 HeapAnalyzerService 中進行,Leakcanary2.0 以後使用 Shark 來解析hprof檔案。

  5. HeapAnalyzer 獲取 hprof中的所有 KeyedWeakReference,並獲取objectId

  6. HeapAnalyzer計算 objectId 到 GC Root 的最短強引用鏈路徑來確定是否有洩漏,然後構建導致洩漏的引用鏈。

  7. 將分析結果儲存在資料庫中,並顯示洩漏通知。

那麼檢測是在什麼時候開始的呢,當然是在 activity, fragment, view, service 等銷燬後才去進行檢測的。下面開始深入程式碼細節。

3.2 LeakCanary 的啟動

在前面介紹使用的時候,我們只是引入了程式碼,都沒呼叫,為啥  LeakCanary 就可以工作了呢?原來 LeakCanary 是使用 ContentProvider 自動初始化的,不需要再手動呼叫 install 方法。可以檢視具體 xml 檔案:

可以看到有個關鍵類 AppWatcherInstaller,下面來看下這個類的具體內容:

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller()

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    // 啟動記憶體檢測  
    AppWatcher.manualInstall(application)
    return true
  }
}

 可以發現 AppWatcherInstaller 繼承自 ContentProvider。當我們啟動App時,一般啟動順序為:

Application->attachBaseContext =====>ContentProvider->onCreate =====>Application->onCreate

ContentProvider會在Application.onCreate前初始化,這樣 AppWatcherInstaller 就會被呼叫。關於 ContentProvider 的啟動流程可以看 Android ContentProvider 啟動分析,這裡就不展開了。在 AppWatcherInstaller 的 onCreate 方法,啟動了 LeakCanary 進行記憶體檢測。

  // AppWatcher 這是一個靜態類  
  @JvmOverloads
  fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),  // 延遲5s
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)  // 這裡就是需要監控的物件
  ) {
    checkMainThread()
    if (isInstalled) {
      throw IllegalStateException(
        "AppWatcher already installed, see exception cause for prior install call", installCause
      )
    }
    check(retainedDelayMillis >= 0) {
      "retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
    }
    installCause = RuntimeException("manualInstall() first called here")
    this.retainedDelayMillis = retainedDelayMillis
    if (application.isDebuggableBuild) {
      LogcatSharkLog.install()
    }
    // Requires AppWatcher.objectWatcher to be set 採用反射形式進行初始化
    LeakCanaryDelegate.loadLeakCanary(application)

    watchersToInstall.forEach {
    // 新增監控物件的回撥 it.install() } }

 manualInstall 是一個很重要的方法,並且其引數也是需要細細看的,第二個引數是延時時間5s,也就是延遲 5s 再去進行記憶體洩漏的檢測。第三個引數就是需要監控物件的list。來看看都有哪些物件:

  fun appDefaultWatchers(
    application: Application,
    reachabilityWatcher: ReachabilityWatcher = objectWatcher
  ): List<InstallableWatcher> {
    return listOf(
      // activity 的監聽
      ActivityWatcher(application, reachabilityWatcher),
      // fragment 的監聽
      FragmentAndViewModelWatcher(application, reachabilityWatcher),
      // view 的監聽
      RootViewWatcher(reachabilityWatcher),
      // service 的監聽
      ServiceWatcher(reachabilityWatcher)
    )
  }

可以看到這裡主要對四個物件進行了監控,分別是

  • activity,通過 Application.ActivityLifecycleCallbacks 來判斷 activity 是否已經銷燬了;

  • fragment,fragment 的不同版本,會有不同的處理,具體可以參考 AndroidSupportFragmentDestroyWatcher, AndroidOFragmentDestroyWatcher,AndroidXFragmentDestroyWatcher 這三個類。其中還包含了對 rootView 的監控

  • rootview,通過 OnRootViewAddedListener 來進行監控,當 android.view.WindowManager.addView 呼叫的時候,會對其 onRootViewAdded 進行回撥,從而可以獲得 rootview 。 

  • service,這裡比較複雜,需要了解相關的原始碼。主要是利用反射來獲取 service 相關的通知。比如獲取到 mH 的 mCallback,並把自己的 callback 交給 mH,這樣當 mh 收到訊息就會回撥 callback,然後再去呼叫攔截的 mCallback,這樣就不會改變原有的執行軌跡。

下面來看下反射的邏輯:

internal object LeakCanaryDelegate {

  @Suppress("UNCHECKED_CAST")
  // 型別是由lazy裡面的程式碼來確定的
  val loadLeakCanary by lazy {
    try {
      val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
      leakCanaryListener.getDeclaredField("INSTANCE")
        .get(null) as (Application) -> Unit  // 將其轉為 (引數)-> unit 型別 
    } catch (ignored: Throwable) {
      NoLeakCanary
    }
  }
}

 可以發現這裡反射來獲取  InternalLeakCanary 的例項,前面呼叫的方式 LeakCanaryDelegate.loadLeakCanary(application),這會觸發 LeakCanaryDelegate 中的 invoke 方法。

那為什麼會觸發呢,因為 LeakCanaryDelegate 繼承了一個函式 

internal object InternalLeakCanary : (Application) -> Unit

 所以下面來看看 invoke 方法:

  override fun invoke(application: Application) {
    _application = application

    checkRunningInDebuggableBuild()
   // 新增回撥
    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

    val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))

    val gcTrigger = GcTrigger.Default

    val configProvider = { LeakCanary.config }
   // 提供一個後臺執行緒的 looper
    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)
   // 初始化 heapDump 觸發器
    heapDumpTrigger = HeapDumpTrigger(
      application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
      configProvider
    )
   // 新增可見性回撥 application.registerVisibilityListener { applicationVisible
-> this.applicationVisible = applicationVisible heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible) }
   // 對 activity 狀態的監聽 registerResumedActivityListener(application) addDynamicShortcut(application)
// We post so that the log happens after Application.onCreate() mainHandler.post { // https://github.com/square/leakcanary/issues/1981 // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref // which blocks until loaded and that creates a StrictMode violation. backgroundHandler.post { SharkLog.d { when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) { is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text) is Nope -> application.getString( R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason() ) } } } } }

可以發現 invoke 才是 LeakCanary 啟動後初始化的核心邏輯。在這裡註冊了很多回撥,啟動了後臺執行緒,heapdump 觸發器,gc 觸發器等。

 到這裡,關於 LeakCanary 的啟動邏輯就講完了。

3.3 如何觸發檢測

其實在講到 LeakCanary 的啟動邏輯的時候,就有提到有四個監控物件,當著四個物件的生命週期發生變化的時候,就會觸發相應的檢測流程。

下面以 ActivityWatcher 為例講述觸發檢測後的邏輯。

class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        // 收到銷燬的回撥,就會觸發下面方法的呼叫
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }
}

其中的 reachabilityWatcher 就是下面這個:

// AppWatcher  
val objectWatcher = ObjectWatcher( // 這裡需要注意的是這是一個靜態變數 clock = { SystemClock.uptimeMillis() }, checkRetainedExecutor = { check(isInstalled) { "AppWatcher not installed" } mainHandler.postDelayed(it, retainedDelayMillis) // 延遲 5s 後執行 excute 操作,這裡 it 個人是覺得指代 excute 方法 }, isEnabled = { true } )

因此,接下去我們需要去看看  ObjectWatcher 這個類的相關邏輯了。

// ObjectWatcher
 @Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {  // 一般為 true
      return
    }
    removeWeaklyReachableObjects() // 先將一些已經回收的監控物件刪除
    val key = UUID.randomUUID().toString() // 獲取唯一的標識
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)  // 建立一個觀察物件
    SharkLog.d {
      "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }

    watchedObjects[key] = reference  // 加入觀察map 中 
    checkRetainedExecutor.execute {
      moveToRetained(key)   // 可以知道, 5s 後才會執行
    }
  }

 expectWeaklyReachable 的所做的事情很簡單,具體如下:

  1. removeWeaklyReachableObjects 先把已經回收的監控物件從 watchedObjects 中刪除;

  2. 通過唯一表示 key,當前時間戳來為當前需要監控的物件構造一個 KeyedWeakReference,並且,所有的監控物件都是共用一個 queue;

  3. 把監控物件新增到 watchedObjects 中;

這裡有個很關鍵的類 KeyedWeakReference,下面來具體看看這個類的實現:

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

還記得前面講的準備知識嗎?這裡就用上了,可以發現 KeyedWeakReference 繼承自 WeakReference,並且新增了一些額外的引數。

這裡通過 activity 為例子介紹了觸發檢測的邏輯,所有監控物件都是在監聽到其被銷燬的時候才會觸發檢測,一旦銷燬了就會把監控物件放在 watchedObjects,等待5s後再來看是否已經被回收。

3.4 回收操作

上文提到 5s 後才會去檢測物件是否已經被回收。現在已經過了 5s 了,來看看監控物件是否已經被回收了。

我們們先來看看 moveToRetained 的具體邏輯:

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

可以看到的是再次呼叫了  removeWeaklyReachableObjects() 方法,也就是5s後,再次對 watchedObjects 的物件進行檢查是否已經被回收了。

不過有一點需要注意的事,並不是對 watchedObjects 進行遍歷來判斷對否回收的,而是從 queue 中取出來物件就表示該物件已經被回收,watchedObjects 中刪除對應的物件即可。

此處還是以 activity 為例子,引數 key 對應的是 activity;這時候會通過 key 來判斷是否可以從 watchedObjects 獲取到物件,如果獲取到物件了,說明該物件依然存活,這時候就會觸發回撥。

可以發現最終是回撥到 InternalLeakCanary 中來的,下面看看相關邏輯:

 // InternalLeakCanary.kt
  override fun onObjectRetained() = scheduleRetainedObjectCheck()

  fun scheduleRetainedObjectCheck() {
    if (this::heapDumpTrigger.isInitialized) {
    // 這裡會對依然存回的物件進行檢測 heapDumpTrigger.scheduleRetainedObjectCheck() } }

這裡呼叫 HeapDumpTrigger 來對存活的物件進行檢測,下面看看具體的檢測邏輯:

// HeapDumpTrigger.kt
  fun scheduleRetainedObjectCheck(
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      return
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
   // 如果從前面一路走下來,delayMillis 是為0的,也就是會立即執行
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects()
    }, delayMillis)
  }

  // 私有的方法,真正的開始檢測
  private fun checkRetainedObjects() {
    val iCanHasHeap = HeapDumpControl.iCanHasHeap() 

    val config = configProvider()

    if (iCanHasHeap is Nope) {  // 也就是此時不能進行 heap dump
      if (iCanHasHeap is NotifyingNope) {
        // Before notifying that we can't dump heap, let's check if we still have retained object.  此時不能進行 heapdump
        var retainedReferenceCount = objectWatcher.retainedObjectCount

        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()  // 觸發gc
          retainedReferenceCount = objectWatcher.retainedObjectCount // 未被回收物件數量
        }

        val nopeReason = iCanHasHeap.reason()
        val wouldDump = !checkRetainedCount(
          retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
        )

        if (wouldDump) {
          val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
          onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
          showRetainedCountNotification(
            objectCount = retainedReferenceCount,
            contentText = uppercaseReason
          )
        }
      } else {
        SharkLog.d {
          application.getString(
            R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
          )
        }
      }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
   // 判斷剩下的數量小於規定數量直接返回,預設是5個起步
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
   // 還未到時間,還需要再等會再進行 heapDump
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { onRetainInstanceListener.onEvent(DumpHappenedRecently) showRetainedCountNotification( objectCount = retainedReferenceCount, contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) ) scheduleRetainedObjectCheck( delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis ) return } dismissRetainedCountNotification() val visibility = if (applicationVisible) "visible" else "not visible" // 進行 heap dump
  dumpHeap( retainedReferenceCount
= retainedReferenceCount, retry = true, reason = "$retainedReferenceCount retained objects, app is $visibility" ) }
上面的程式碼比較長,整理下相關知識點:
  1. 如果 retainedObjectCount 數量大於0,則進行一次 GC,避免額外的 Dump,可以儘可能的將物件回收;

  2. 預設情況下,如果 retainedReferenceCount<5,不會進行 Dump,節省資源

  3. 如果兩次 Dump 之間時間少於60s,也會直接返回,避免頻繁 Dump

  4. 呼叫 dumpHeap()進行真正的 Dump 操作

  5. 當然在真正進行 dump 前,還需要依賴 ICanHazHeap 來判斷是否可以進行 heapdump,裡面會做一些檢查,確保 heapdump 的條件是滿足的

 ICanHazHeap 類很有趣,採用了 sealed,可以理解為是列舉類。

  // HeapDumpControl.kt
  sealed class ICanHazHeap {
    object Yup : ICanHazHeap()
    abstract class Nope(val reason: () -> String) : ICanHazHeap()
    class SilentNope(reason: () -> String) : Nope(reason)

    /**
     * Allows manual dumping via a notification
     */
    class NotifyingNope(reason: () -> String) : Nope(reason)
  }

簡單來說就是定義了幾種不同型別的情況,比如 Nope 是 不可以 的意思,Yup 是 可以不錯 的意思,其他兩個類似。因此只有在 Yup 下才可以進行 heap dump 。

3.5 heap dump

上文講完了回收部分,對於實在無法被回收的,這時候就採用 heap dump 來將其現出原形。

// HeapDumpTrigger.kt  
private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    when (val heapDumpResult = heapDumper.dumpHeap()) {
      is NoHeapDump -> {  // 沒有dump
        if (retry) {
          SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
          scheduleRetainedObjectCheck(
            delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
          )
        } else {
          SharkLog.d { "Failed to dump heap, will not automatically retry" }
        }
        showRetainedCountNotification(  // 顯示 dump 失敗通知
          objectCount = retainedReferenceCount,
          contentText = application.getString(
            R.string.leak_canary_notification_retained_dump_failed
          )
        )
      }
      is HeapDump -> {  // dump 成功
        lastDisplayedRetainedObjectCount = 0
        lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
        objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
        HeapAnalyzerService.runAnalysis(
          context = application,
          heapDumpFile = heapDumpResult.file,
          heapDumpDurationMillis = heapDumpResult.durationMillis,
          heapDumpReason = reason
        )
      }
    }
  }

 HeapDumpTrigger 如其名,就是一個 dump 觸發器,這裡最終是呼叫 AndroidHeapDumper 來進行 dump 的,最後會得到 dump 的結果。

可以看到上述主要講結果分為兩類,一個是 NoHeapDump,如果需要繼續嘗試的話,會延遲一段時間後繼續重試。另一個結果自然就是成功了。

暫時先不看結果,這裡先來看看 AndroidHeapDumper dump 過程,具體程式碼如下:

 Debug 是 android 系統自帶的方法,最終也會呼叫 VMDebug 來實現,這個其實就是虛擬機器的了。

// android.os.Debug.java   
 public static void dumpHprofData(String fileName) throws IOException {
        VMDebug.dumpHprofData(fileName);
    }

 前文提到的兩種結果,其實都是繼承自 DumpHeapResult,其中 HeapDump 的資料結構如下:

internal data class HeapDump(
  val file: File,
  val durationMillis: Long
) : DumpHeapResult()

當 dump 成功知乎,就是對 hprof 檔案的分析了。Leakcanary2.0版本開源了自己實現的 hprof 檔案解析以及洩漏引用鏈查詢的功能模組(命名為shark)。

分析hprof檔案的工作主要是在 HeapAnalyzerService 類中完成的。

// HeapAnalyzerService.kt   
 fun runAnalysis(
      context: Context,
      heapDumpFile: File,
      heapDumpDurationMillis: Long? = null,
      heapDumpReason: String = "Unknown"
    ) {
      val intent = Intent(context, HeapAnalyzerService::class.java)
      intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
      intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason)
      heapDumpDurationMillis?.let {
        intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis)
      }
      startForegroundService(context, intent)
    }

 可以看到這裡啟動了一個後臺service 來對資料進行解析。本文由於篇幅有限,就不再講述後面分析的邏輯,關於 hprof 後面有時間會再進行分析。

其他

關於如何修復記憶體洩漏的篇章中,LeakCanary 給了下面一個簡單提醒。

很多人都把弱引用來替換強引用來解決所謂的記憶體洩漏,這也是修復方式最快的一種。然而 LeakCanary 並不認可這種方式。因為記憶體洩漏問題的本質是被引用物件存活時間超過了其生命週期,也就是他不能被正確銷燬。但是你把強引用改成弱引用,會使得部分物件的存活時間短短小於原本的生命週期,而這可能會引發更多的bug,同時也會使得程式碼更加難以維護。 

比如很多業務 api 都會讓使用者註冊監聽某個結果的回撥,但是卻沒有提供移除監聽的方法,一旦出現記憶體洩漏,大家就會採用弱引用進行封裝,但是由於垃圾回收的存在,可能會導致呼叫方無法收到結果的回撥。還有就是如果業務程式碼寫得不夠好,就會出現空指標的問題。 

總結

 看完本文相信大家都對 LeakCanary 記憶體洩漏檢測原理有了一定的瞭解。可以試著回答下面幾個問題來加深對 LeakCanary 檢測原理的理解。

  1. LeakCanary 檢測原理是什麼?可以參考前面的準備知識部分。

  2. LeakCanary 為啥引入依賴後就可以自己進行記憶體檢測?

  3. LeakCanary 都對哪些物件進行了監控,怎麼實現的監控?

  4. LeakCanary 在什麼時候回觸發記憶體洩漏檢測?是定時的還是其他什麼策略?

  5. LeakCanary 怎麼判斷一個物件發生了記憶體洩露的?(多次GC後物件依然沒有被回收)

  6. LeakCanary 什麼時候才會去進行 dump 操作?dump 操作是為了獲取什麼?

如果上述問題你都能回答出來,那麼恭喜你,你已經入門 LeakCanary 了。

參考文章

java 原始碼系列 - 帶你讀懂 Reference 和 ReferenceQueue

【帶著問題學】關於LeakCanary2.0你應該知道的知識點

LeakCanary原理分析

相關文章