全新 LeakCanary 2 ! 完全基於 Kotlin 重構升級 !

大概一年以前,寫過一篇 LeakCanary 源碼解析 ,當時是基於 1.5.4 版本進行分析的 。Square 公司在今年四月份發佈了全新的 2.0 版本,完全使用 Kotlin 進行重構,核心原理並沒有太大變化,但是做了一定的性能優化。在本文中,就讓我們通過源碼來看看 2.0 版本發生了哪些變化。本文不會過多的分析源碼細節,詳細細節可以閱讀我之前基於 1.5.4 版本寫的文章,兩個版本在原理方面並沒有太大變化。

含註釋 fork 版本 LeakCanary 源碼

使用

首先來對比一下兩個版本的使用方式。

1.5.4 版本

在老版本中,我們需要添加如下依賴:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

leakcanary-android-no-op 庫在 release 版本中使用,其中是沒有任何邏輯代碼的。

然後需要在自己的 Application 中進行初始化。

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

LeakCanary.install() 執行後,就會構建 RefWatcher 對象,開始監聽 Activity.onDestroy() 回調, 通過 RefWatcher.watch() 監測 Activity 引用的泄露情況。發現內存泄露之後進行 heap dump ,利用 Square 公司的另一個庫 haha(已廢棄)來分析 heap dump 文件,找到引用鏈之後通知用戶。這一套原理在新版本中還是沒變的。

2.0 版本

新版本的使用更加方便了,你只需要在 build.gradle 文件中添加如下依賴:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

是的,你沒看過,這樣就可以了。你肯定會有一個疑問,那它是如何初始化的呢?我剛看到這個使用文檔的時候,同樣也有這個疑問。當你看看源碼之後就一目瞭然了。我先不解釋,看一下源碼中的 LeakSentryInstaller 這個類:

/**
 * Content providers are loaded before the application class is created. [LeakSentryInstaller] is
 * used to install [leaksentry.LeakSentry] on application start.
 *
 * Content Provider 在 Application 創建之前被自動加載,因此無需用戶手動在 onCrate() 中進行初始化
 */
internal class LeakSentryInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    CanaryLog.logger = DefaultCanaryLog()
    val application = context!!.applicationContext as Application
    InternalLeakSentry.install(application) // 進行初始化工作,核心
    return true
  }

  override fun query(
    uri: Uri,
    strings: Array<String>?,
    s: String?,
    strings1: Array<String>?,
    s1: String?
  ): Cursor? {
    return null
  }

  override fun getType(uri: Uri): String? {
    return null
  }

  override fun insert(
    uri: Uri,
    contentValues: ContentValues?
  ): Uri? {
    return null
  }

  override fun delete(
    uri: Uri,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }

  override fun update(
    uri: Uri,
    contentValues: ContentValues?,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }
}

看到這個類你應該也明白了。LeakCanary 利用 ContentProvier 進行了初始化。ContentProvier 一般會在 Application 被創建之前被加載,LeakCanary 在其 onCreate() 方法中調用了 InternalLeakSentry.install(application) 進行初始化。這應該是我第一次看到第三方庫這麼進行初始化。這的確是方便了開發者,但是仔細想想弊端還是很大的,如果所有第三方庫都這麼幹,開發者就沒法控制應用啓動時間了。很多開發者爲了加快應用啓動速度,都下了很大心血,包括按需延遲初始化第三方庫。但在 LeakCanary 中,這個問題並不存在,因爲它本身就是一個只在 debug 版本中使用的庫,並不會對 release 版本有任何影響。

源碼解析

前面提到了 InternalLeakSentry.install() 就是核心的初始化工作,其地位就和 1.5.4 版本中的 LeakCanary.install() 一樣。下面就從 install() 方法開始,走進 LeakCanary 2.0 一探究竟。

1. LeakCanary.install()

fun install(application: Application) {
    CanaryLog.d("Installing LeakSentry")
    checkMainThread() // 只能在主線程調用,否則會拋出異常
    if (this::application.isInitialized) {
      return
    }
    InternalLeakSentry.application = application

    val configProvider = { LeakSentry.config }
    ActivityDestroyWatcher.install( // 監聽 Activity.onDestroy(),見 1.1
        application, refWatcher, configProvider
    )
    FragmentDestroyWatcher.install( // 監聽 Fragment.onDestroy(),見 1.2
        application, refWatcher, configProvider
    )
    listener.onLeakSentryInstalled(application) // 見 1.3
}

install() 方法主要做了三件事:

  • 註冊 Activity.onDestroy() 監聽
  • 註冊 Fragment.onDestroy() 監聽
  • 監聽完成後進行一些初始化工作

依次看一看。

1.1 ActivityDestroyWatcher.install()

ActivityDestroyWatcher 類的源碼很簡單:

internal class ActivityDestroyWatcher private constructor(
  private val refWatcher: RefWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
    override fun onActivityDestroyed(activity: Activity) {
      if (configProvider().watchActivities) {
        refWatcher.watch(activity) // 監聽到 onDestroy() 之後,通過 refWatcher 監測 Activity
      }
    }
  }

  companion object {
    fun install(
      application: Application,
      refWatcher: RefWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(refWatcher, configProvider)
      // 註冊 Activity 生命週期監聽
      application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks) 
    }
  }
}

install() 方法中註冊了 Activity 生命週期監聽,在監聽到 onDestroy() 時,調用 RefWatcher.watch() 方法開始監測 Activity。

1.2 FragmentDestroyWatcher.install()

FragmentDestroyWatcher 是一個接口,它有兩個實現類 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher

internal interface FragmentDestroyWatcher {

  fun watchFragments(activity: Activity)

  companion object {

    private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"

    fun install(
      application: Application,
      refWatcher: RefWatcher,
      configProvider: () -> LeakSentry.Config
    ) {
      val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()

      if (SDK_INT >= O) { // >= 26,使用 AndroidOFragmentDestroyWatcher
        fragmentDestroyWatchers.add(
            AndroidOFragmentDestroyWatcher(refWatcher, configProvider)
        )
      }

      if (classAvailable(
              SUPPORT_FRAGMENT_CLASS_NAME
          )
      ) {
        fragmentDestroyWatchers.add( // androidx 使用 SupportFragmentDestroyWatcher
            SupportFragmentDestroyWatcher(refWatcher, configProvider)
        )
      }

      if (fragmentDestroyWatchers.size == 0) {
        return
      }

      application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
        override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
        ) {
          for (watcher in fragmentDestroyWatchers) {
            watcher.watchFragments(activity)
          }
        }
      })
    }

    private fun classAvailable(className: String): Boolean {
      return try {
        Class.forName(className)
        true
      } catch (e: ClassNotFoundException) {
        false
      }
    }
  }
}

如果我沒記錯的話,1.5.4 是不監測 Fragment 的泄露的。而 2.0 版本提供了對 Android O 以及 androidx 版本中的 Fragment 的內存泄露檢測。 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher 的實現代碼其實是一致的,Android O 及以後,androidx 都具備對 Fragment 生命週期的監聽功能。以 AndroidOFragmentDestroyWatcher 爲例,簡單看一下它的實現。

@RequiresApi(Build.VERSION_CODES.O) //
internal class AndroidOFragmentDestroyWatcher(
  private val refWatcher: RefWatcher,
  private val configProvider: () -> Config
) : FragmentDestroyWatcher {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null && configProvider().watchFragmentViews) {
        refWatcher.watch(view)
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      if (configProvider().watchFragments) {
        refWatcher.watch(fragment)
      }
    }
  }

  override fun watchFragments(activity: Activity) {
    val fragmentManager = activity.fragmentManager
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
  }
}

同樣,還是使用 RefWatcher.watch() 方法來進行監測。

1.3 listener.onLeakSentryInstalled()

onLeakSentryInstalled() 回調中會初始化一些檢測內存泄露過程中需要的對象,如下所示:

override fun onLeakSentryInstalled(application: Application) {
    this.application = application

    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) // 用於 heap dump

    val gcTrigger = GcTrigger.Default // 用於手動調用 GC

    val configProvider = { LeakCanary.config } // 配置項

    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper) // 發起內存泄漏檢測的線程

    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
    )
    application.registerVisibilityListener { applicationVisible ->
      this.applicationVisible = applicationVisible
      heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
    }
    addDynamicShortcut(application)
}

對老版本代碼熟悉的同學,看到這些對象應該很熟悉。

  • heapDumper 用於確認內存泄漏之後進行 heap dump 工作。
  • gcTrigger 用於發現可能的內存泄漏之後手動調用 GC 確認是否真的爲內存泄露。

這兩個對象是 LeakCanary 檢測內存泄漏的核心。後面會進行詳細分析。

到這裏,整個 LeakCanary 的初始化工作就完成了。與 1.5.4 版本不同的是,新版本增加了對 Fragment 以及 androidx 的支持。當發生 Activity.onDestroy()Fragment.onFragmentViewDestroyed() , Fragment.onFragmentDestroyed() 三者之一時,RefWatcher 就開始工作了,調用其 watch() 方法開始檢測引用是否泄露。

2. RefWatcher.watch()

在看源碼之前,我們先來看幾個後面會使用到的隊列。

  /**
   * References passed to [watch] that haven't made it to [retainedReferences] yet.
   * watch() 方法傳進來的引用,尚未判定爲泄露
   */
  private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
  /**
   * References passed to [watch] that we have determined to be retained longer than they should
   * have been.
   * watch() 方法傳進來的引用,已經被判定爲泄露
   */
  private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
  private val queue = ReferenceQueue<Any>() // 引用隊列,配合弱引用使用

通過 watch() 方法傳入的引用都會保存在 watchedReferences 中,被判定泄露之後保存在 retainedReferences 中。注意,這裏的判定過程不止會發生一次,已經進入隊列 retainedReferences 的引用仍有可能被移除。queue 是一個 ReferenceQueue 引用隊列,配合弱引用使用,這裏記住一句話:

弱引用一旦變得弱可達,就會立即入隊。這將在 finalization 或者 GC 之前發生。

也就是說,會被 GC 回收的對象引用,會保存在隊列 queue 中。

回頭再來看看 watch() 方法的源碼。

  @Synchronized fun watch(
    watchedReference: Any,
    referenceName: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableReferences() // 移除隊列中將要被 GC 的引用,見 2.1
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference = // 構建當前引用的弱引用對象,並關聯引用隊列 queue
      KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
    if (referenceName != "") {
      CanaryLog.d(
          "Watching instance of %s named %s with key %s", reference.className,
          referenceName, key
      )
    } else {
      CanaryLog.d(
          "Watching instance of %s with key %s", reference.className, key
      )
    }

    watchedReferences[key] = reference // 將引用存入 watchedReferences
    checkRetainedExecutor.execute {
      moveToRetained(key) // 如果當前引用未被移除,仍在 watchedReferences  隊列中,
                          // 說明仍未被 GC,移入 retainedReferences 隊列中,暫時標記爲泄露
                          // 見 2.2
    }
  }

邏輯還是比較清晰的,首先會調用 removeWeaklyReachableReferences() 方法,這個方法在整個過程中會多次調用。其作用是移除 watchedReferences 中將被 GC 的引用。

2.1 removeWeaklyReachableReferences()

  private fun removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    // 弱引用一旦變得弱可達,就會立即入隊。這將在 finalization 或者 GC 之前發生。
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference? // 隊列 queue 中的對象都是會被 GC 的
      if (ref != null) {
        val removedRef = watchedReferences.remove(ref.key)
        if (removedRef == null) {
          retainedReferences.remove(ref.key)
        }
        // 移除 watchedReferences 隊列中的會被 GC 的 ref 對象,剩下的就是可能泄露的對象
      }
    } while (ref != null)
  }

整個過程中會多次調用,以確保將已經入隊 queue 的將被 GC 的對象引用移除掉,避免無謂的 heap dump 操作。而仍在 watchedReferences 隊列中的引用,則可能已經泄露,移到隊列 retainedReferences 中,這就是 moveToRetained() 方法的邏輯。代碼如下:

2.2 moveToRetained()

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableReferences() // 再次調用,防止遺漏
    val retainedRef = watchedReferences.remove(key)
    if (retainedRef != null) {
      retainedReferences[key] = retainedRef
      onReferenceRetained()
    }
  }

這裏的 onReferenceRetained() 最後會回調到 InternalLeakCanary.kt 中。

  override fun onReferenceRetained() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onReferenceRetained()
    }
  }

調用了 HeapDumpTriggeronReferenceRetained() 方法。

  fun onReferenceRetained() {
    scheduleRetainedInstanceCheck("found new instance retained")
  }
  
    private fun scheduleRetainedInstanceCheck(reason: String) {
    if (checkScheduled) {
      return
    }
    checkScheduled = true
    backgroundHandler.post {
      checkScheduled = false
      checkRetainedInstances(reason) // 檢測泄露實例
    }
  }

checkRetainedInstances() 方法是確定泄露的最後一個方法了。這裏會確認引用是否真的泄露,如果真的泄露,則發起 heap dump,分析 dump 文件,找到引用鏈,最後通知用戶。整體流程和老版本是一致的,但在一些細節處理,以及 dump 文件的分析上有所區別。下面還是通過源碼來看看這些區別。

  private fun checkRetainedInstances(reason: String) {
    CanaryLog.d("Checking retained instances because %s", reason)
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if (!config.dumpHeap) {
      return
    }

    var retainedKeys = refWatcher.retainedKeys

    // 當前泄露實例個數小於 5 個,不進行 heap dump
    if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      showRetainedCountWithDebuggerAttached(retainedKeys.size)
      scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
      CanaryLog.d(
          "Not checking for leaks while the debugger is attached, will retry in %d ms",
          WAIT_FOR_DEBUG_MILLIS
      )
      return
    }

    // 可能存在被觀察的引用將要變得弱可達,但是還未入隊引用隊列。
    // 這時候應該主動調用一次 GC,可能可以避免一次 heap dump
    gcTrigger.runGc()

    retainedKeys = refWatcher.retainedKeys

    if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

    HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)

    CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
    HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
    dismissNotification()
    val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
    if (heapDumpFile == null) {
      CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
      scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
      showRetainedCountWithHeapDumpFailed(retainedKeys.size)
      return
    }

    refWatcher.removeRetainedKeys(retainedKeys) // 移除已經 heap dump 的 retainedKeys

    HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
  }

首先調用 checkRetainedCount() 函數判斷當前泄露實例個數如果小於 5 個,僅僅只是給用戶一個通知,不會進行 heap dump 操作,並在 5s 後再次發起檢測。這是和老版本一個不同的地方。

  private fun checkRetainedCount(
    retainedKeys: Set<String>,
    retainedVisibleThreshold: Int // 默認爲 5 個
  ): Boolean {
    if (retainedKeys.isEmpty()) {
      CanaryLog.d("No retained instances")
      dismissNotification()
      return true
    }

    if (retainedKeys.size < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        CanaryLog.d(
            "Found %d retained instances, which is less than the visible threshold of %d",
            retainedKeys.size,
            retainedVisibleThreshold
        )
        // 通知用戶 "App visible, waiting until 5 retained instances"
        showRetainedCountBelowThresholdNotification(retainedKeys.size, retainedVisibleThreshold)
        scheduleRetainedInstanceCheck( // 5s 後再次發起檢測
            "Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
  }

當集齊 5 個泄露實例之後,也並不會立馬進行 heap dump。而是先手動調用一次 GC。當然不是使用 System.gc(),如下所示:

  object Default : GcTrigger {
    override fun runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perform a gc.
      Runtime.getRuntime()
          .gc()
      enqueueReferences()
      System.runFinalization()
    }

那麼,爲什麼要進行這次 GC 呢?可能存在被觀察的引用將要變得弱可達,但是還未入隊引用隊列的情況。這時候應該主動調用一次 GC,可能可以避免一次額外的 heap dump 。GC 之後再次調用 checkRetainedCount() 判斷泄露實例個數。如果此時仍然滿足條件,就要發起 heap dump 操作了。具體邏輯在 AndroidHeapDumper.dumpHeap() 方法中,核心方法就是下面這句代碼:

Debug.dumpHprofData(heapDumpFile.absolutePath)

生成 heap dump 文件之後,要刪除已經處理過的引用,

refWatcher.removeRetainedKeys(retainedKeys)

最後啓動一個前臺服務 HeapAnalyzerService 來分析 heap dump 文件。老版本中是使用 Square 自己的 haha 庫來解析的,這個庫已經廢棄了,Square 完全重寫了解析庫,主要邏輯都在 moudle leakcanary-analyzer 中。這部分我還沒有閱讀,就不在這裏分析了。對於新的解析器,官網是這樣介紹的:

Uses 90% less memory and 6 times faster than the prior heap parser.

減少了 90% 的內存佔用,而且比原來快了 6 倍。後面有時間單獨來分析一下這個解析庫。

後面的過程就不再贅述了,通過解析庫找到最短 GC Roots 引用路徑,然後展示給用戶。

總結

通讀完源碼,LeakCanary 2 還是帶來了很多的優化。與老版本相比,主要有以下不同:

  • 百分之百使用 Kotlin 重寫
  • 自動初始化,無需用戶手動再添加初始化代碼
  • 支持 fragment,支持 androidx
  • 當泄露引用到達 5 個時纔會發起 heap dump
  • 全新的 heap parser,減少 90% 內存佔用,提升 6 倍速度

文章首發微信公衆號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解。

更多相關知識,掃碼關注我吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章