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

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 { // //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原理分析

Tags: