死磕Android_LeakCanary原理賞析

本文基於 leakcanary-android:2.5

我的所有原創Android知識體系,已打包整理到GitHub.努力打造一系列適合初中高級工程師能夠看得懂的優質文章,歡迎star~

思維導圖

在這裏插入圖片描述

1. 背景

Android開發中,內存泄露時常有發生在,有可能是你自己寫的,也有可能是三方庫裏面的.程序中已動態分配的堆內存由於某種特殊原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至程序崩潰等嚴重後果.本來Android內存就喫緊,還內存泄露的話,後果不堪設想.所以我們要儘量避免內存泄露,一方面我們要學習哪些常見場景下會發生內存泄露,一方面我們引入LeakCanary幫我們自動檢測有內存泄露的地方.

LeakCanary是Square公司(對,又是這個公司,OkHttp和Retrofit等都是這家公司開源的)開源的一個庫,通過它我們可以在App運行的過程中檢測內存泄露,它把對象內存泄露的引用鏈也給開發人員分析出來了,我們去修復這個內存泄露非常方面.

ps: LeakCanary直譯過來是內存泄露的金絲雀,關於這個名字其實有一個小故事在裏面.金絲雀,美麗的鳥兒.她的歌聲不僅動聽,還曾挽救過無數礦工的生命.17世紀,英國礦井工人發現,金絲雀對瓦斯這種氣體十分敏感.空氣中哪怕有極其微量的瓦斯,金絲雀也會停止歌唱;而當瓦斯含量超過一定限度時,雖然魯鈍的人類毫無察覺,金絲雀卻早已毒發身亡.當時在採礦設備相對簡陋的條件下,工人們每次下井都會帶上一隻金絲雀作爲"瓦斯檢測指標",以便在危險狀況下緊急撤離. 同樣的,LeakCanary這隻"金絲雀"能非常敏感地幫我們發現內存泄露,從而避免OOM的風險.

2. 初始化

在引入LeakCanary的時候,只需要在build.gradle中加入下面這行配置即可:

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

That’s it, there is no code change needed! 我們不需要改動任何的代碼,就這樣,LeakCanary就已經引入進來了. 那我有疑問了?我們一般引入一個庫都是在Application的onCreate中初始化,它不需要在代碼中初始化,它是如何起作用的呢?

我只想到一種方案可以實現這個,就是它在內部定義了一個ContentProvider,然後在ContentProvider的裏面進行的初始化.

咱驗證一下: 引入LeakCanary之後,運行一下項目,然後在debug的apk裏面查看AndroidManifest文件,搜一下provider定義.果然,我找到了:

<provider
    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
    android:enabled="@ref/0x7f040007"
    android:exported="false"
    android:authorities="com.xfhy.allinone.leakcanary-installer" />
<!--這裏的@ref/0x7f040007對應的是@bool/leak_canary_watcher_auto_install-->
class AppWatcherInstaller : ContentProvider() {
   
   
    override fun onCreate(): Boolean {
   
   
        val application = context!!.applicationContext as Application
        AppWatcher.manualInstall(application)
        return true
    }
}

哈哈,果然是在ContentProvider裏面進行的初始化.App在啓動時會自動初始化ContentProvider,也就自動調用了AppWatcher.manualInstall()進行了初始化.一開始的時候,我覺得這樣挺好的,挺優雅,後來發現好多三方庫都這麼幹了.每個庫一個ContentProvider進行初始化,有點冗餘的感覺.後來Jetpack推出了App Startup,解決了這個問題,它就是基於這個原理進行的封裝.

需要注意的是ContentProvider的onCreate執行時機比Application的onCreate執行時機還早.如果你想在其他時機進行初始化優化啓動時間,也是可以的.只需要在app裏重寫@bool/leak_canary_watcher_auto_install的值爲false即可.然後手動在合適的地方調用AppWatcher.manualInstall(application).但是LeakCanary本來就是在debug的時候用的,所以感覺優化啓動時間不是那麼必要.

3. 監聽泄露的時機

LeakCanary自動檢測以下對象的泄露:

  • destroyed Activity instances
  • destroyed Fragment instances
  • destroyed fragment View instances
  • cleared ViewModel instances

可以看到,檢測的都是些Android開發中容易被泄露的東西.那麼它是如何檢測的,下面我們來分析一下

3.1 Activity

通過Application#registerActivityLifecycleCallbacks()註冊Activity生命週期監聽,然後在onActivityDestroyed()中進行objectWatcher.watch(activity,....)進行檢測對象是否泄露.檢測對象是否泄露這塊後面單獨分析.

3.2 Fragment、Fragment View

同樣的,檢測這2個也是需要監聽週期,不過這次監聽的是Fragment的生命週期,利用fragmentManager.registerFragmentLifecycleCallbacks可以實現.Fragment是在onFragmentDestroy()中檢測Fragment對象是否泄露,Fragment View在onFragmentViewDestroyed()裏面檢測Fragment View對象是否泄露.

但是,拿到這個fragmentManager的過程有點曲折.

  • Android O以上,通過activity#getFragmentManager()獲得. (AndroidOFragmentDestroyWatcher)
  • AndroidX中,通過activity#getSupportFragmentManager()獲得. (AndroidXFragmentDestroyWatcher)
  • support包中,通過activity#getSupportFragmentManager()獲得. (AndroidSupportFragmentDestroyWatcher)

可以看到,不同的場景下,取FragmentManager的方式是不同的.取FragmentManager的實現過程、註冊Fragment生命週期、在onFragmentDestroyed和onFragmentViewDestroyed中檢測對象是否有泄漏這一套邏輯,在不同的環境下,實現不同.所以把它們封裝進不同的策略(對應着上面3種策略)中,這就是策略模式的應用.

因爲上面獲取FragmentManager需要Activity實例,所以這裏還需要監聽Activity生命週期,在onActivityCreated()中拿到Activity實例,從而拿到FragmentManager去監聽Fragment生命週期.

//AndroidOFragmentDestroyWatcher.kt

override fun onFragmentViewDestroyed(
  fm: FragmentManager,
  fragment: Fragment
) {
   
   
  val view = fragment.view
  if (view != null && configProvider().watchFragmentViews) {
   
   
    objectWatcher.watch(
        view, "${
     
     fragment::class.java.name} received Fragment#onDestroyView() callback " +
        "(references to its views should be cleared to prevent leaks)"
    )
  }
}

override fun onFragmentDestroyed(
  fm: FragmentManager,
  fragment: Fragment
) {
   
   
  if (configProvider().watchFragments) {
   
   
    objectWatcher.watch(
        fragment, "${
     
     fragment::class.java.name} received Fragment#onDestroy() callback"
    )
  }
}

3.3 ViewModel

在前面講到的AndroidXFragmentDestroyWatcher中還會單獨監聽onFragmentCreated()

override fun onFragmentCreated(
  fm: FragmentManager,
  fragment: Fragment,
  savedInstanceState: Bundle?
) {
   
   
  ViewModelClearedWatcher.install(fragment, objectWatcher, configProvider)
}

install裏面實際是通過fragment和ViewModelProvider生成一個ViewModelClearedWatcher,這是一個新的ViewModel,然後在這個ViewModel的onCleared()裏面檢測這個fragment裏面的每個ViewModel是否存在泄漏

//ViewModelClearedWatcher.kt

init {
   
   
    // We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,
    // however that was added in 2.1.0 and we support AndroidX first stable release. viewmodel-2.0.0
    // does not have ViewModelStore#keys. All versions currently have the mMap field.
    //通過反射拿到該fragment的所有ViewModel
    viewModelMap = try {
   
   
      val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
      mMapField.isAccessible = true
      @Suppress("UNCHECKED_CAST")
      mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) {
   
   
      null
    }
  }

  override fun onCleared() {
   
   
    if (viewModelMap != null && configProvider().watchViewModels) {
   
   
      viewModelMap.values.forEach {
   
    viewModel ->
        objectWatcher.watch(
            viewModel, "${
     
     viewModel::class.java.name} received ViewModel#onCleared() callback"
        )
      }
    }
  }

4. 監測對象是否泄露

在講這個之前得先回顧一個知識點,Java中的WeakReference是弱引用類型,每當發生GC時,它所持有的對象如果沒有被其他強引用所持有,那麼它所引用的對象就會被回收,同時或者稍後的時間這個WeakReference會被入隊到ReferenceQueue中.LeakCanary中檢測內存泄露就是基於這個原理.

/**
 * Weak reference objects, which do not prevent their referents from being
 * made finalizable, finalized, and then reclaimed.  Weak references are most
 * often used to implement canonicalizing mappings.
 *
 * <p> Suppose that the garbage collector determines at a certain point in time
 * that an object is <a href="package-summary.html#reachability">weakly
 * reachable</a>.  At that time it will atomically clear all weak references to
 * that object and all weak references to any other weakly-reachable objects
 * from which that object is reachable through a chain of strong and soft
 * references.  At the same time it will declare all of the formerly
 * weakly-reachable objects to be finalizable.  At the same time or at some
 * later time it will enqueue those newly-cleared weak references that are
 * registered with reference queues.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */

public class WeakReference<T> extends Reference<T> {
   
   

    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
   
   
        super(referent, q);
    }

}

實現要點:

  1. 當一個對象需要被回收時,生成一個唯一的key,將它們封裝進KeyedWeakReference中,並傳入自定義的ReferenceQueue
  2. 將key和KeyedWeakReference放入一個map中
  3. 過一會兒之後(默認是5秒)主動觸發GC,將自定義的ReferenceQueue中的KeyedWeakReference全部移除(它們所引用的對象已被回收),並同時根據這些KeyedWeakReference的key將map中的KeyedWeakReference也移除掉.
  4. 此時如果map中還有KeyedWeakReference剩餘,那麼就是沒有入隊的,也就是說這些KeyedWeakReference所對應的對象還沒被回收.這是不合理的,這裏就產生了內存泄露.
  5. 將這些內存泄露的對象分析引用鏈,保存數據

下面來看具體代碼:

//ObjectWatcher.kt

/**
* Watches the provided [watchedObject].
*
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
    ......
    //移除引用隊列中的所有KeyedWeakReference,同時也將其從map中移除
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)

    //存入map    
    watchedObjects[key] = reference
    
    //默認5秒之後執行moveToRetained()檢查
    //這裏是用的handler.postDelay實現的延遲
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
}

@Synchronized private fun moveToRetained(key: String) {
    //移除那些已經被回收的
    removeWeaklyReachableObjects()
    //判斷一下這個key鎖對應的KeyedWeakReference是否被移除了
    val retainedRef = watchedObjects[key]
    //沒有被移除的話,說明是發生內存泄露了
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
}

需要被回收的Activity、Fragment什麼的都會走watch()這個方法這裏,檢測是否有內存泄露發生.上面這塊代碼對應着實現要點的1-4步.接下來具體分析內存泄露了是怎麼走的

//InternalLeakCanary#onObjectRetained()
//InternalLeakCanary#scheduleRetainedObjectCheck()
//HeapDumpTrigger#scheduleRetainedObjectCheck()
//HeapDumpTrigger#checkRetainedObjects()

private fun checkRetainedObjects() {
   
   
    //比如如果是在調試,那麼暫時先不dump heap,延遲20秒再判斷一下狀態

    val config = configProvider()
    
    ......
    //還剩多少對象沒被回收  這些對象可能不是已經泄露的
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
   
   
      //手動觸發GC,這裏觸發GC時還延遲了100ms,給那些回收了的對象入引用隊列一點時間,好讓結果更準確.
      gcTrigger.runGc()
      //再看看還剩多少對象沒被回收
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    
    //checkRetainedCount這裏有2中情況返回true,流程return.
    //1. 未被回收的對象數是0,展示無泄漏的通知
    //2. 當retainedReferenceCount小於5個,展示有泄漏的通知(app可見或不可見超過5秒),延遲2秒再進行檢查checkRetainedObjects()
    //app可見是在VisibilityTracker.kt中判斷的,通過記錄Activity#onStart和onStop的數量來判斷
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
   
   
      //1分鐘之內才dump過,再過會兒再來
      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
    }

    //開始dump
    //通過 Debug.dumpHprofData(filePath)  dump heap
    //開始dump heap之前還得objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) 清除一下這次dump開始之前的所有引用
    //最後是用HeapAnalyzerService這個IntentService去分析heap,具體在HeapAnalyzerService#runAnalysis()
    dumpHeap(retainedReferenceCount, retry = true)
  }

HeapAnalyzerService 裏調用的是 Shark 庫對 heap 進行分析,分析的結果再返回到 DefaultOnHeapAnalyzedListener.onHeapAnalyzed 進行分析結果入庫、發送通知消息。

Shark 🦈 :Shark is the heap analyzer that powers LeakCanary 2. It’s a Kotlin standalone heap analysis library that runs at 「high speed」 with a 「low memory footprint」.

5. 總結

LeakCanary是一隻優雅的金絲雀,幫助我們監測內存泄露.本文主要分析了LeakCanary的初始化、監聽泄露的時機、監測某個對象泄露的過程.源碼中實現非常優雅,本文中未完全展現出來,比較源碼太多貼上來不太雅觀.讀源碼不僅能讓我們學到新東西,而且也讓我們以後寫代碼有可以模仿的對象,甚至還可以在面試時得心應手,一舉三得.

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