死磕Android-LeakCanary原理了解一下?

本文基于 leakcanary-android:2.5

思維導(dǎo)圖

1. 背景

Android開發(fā)中,內(nèi)存泄露時(shí)常有發(fā)生在,有可能是你自己寫的,也有可能是三方庫里面的.程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種特殊原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至程序崩潰等嚴(yán)重后果.本來Android內(nèi)存就吃緊,還內(nèi)存泄露的話,后果不堪設(shè)想.所以我們要盡量避免內(nèi)存泄露,一方面我們要學(xué)習(xí)哪些常見場(chǎng)景下會(huì)發(fā)生內(nèi)存泄露,一方面我們引入LeakCanary幫我們自動(dòng)檢測(cè)有內(nèi)存泄露的地方.

LeakCanary是Square公司(對(duì),又是這個(gè)公司,OkHttp和Retrofit等都是這家公司開源的)開源的一個(gè)庫,通過它我們可以在App運(yùn)行的過程中檢測(cè)內(nèi)存泄露,它把對(duì)象內(nèi)存泄露的引用鏈也給開發(fā)人員分析出來了,我們?nèi)バ迯?fù)這個(gè)內(nèi)存泄露非常方面.

ps: LeakCanary直譯過來是內(nèi)存泄露的金絲雀,關(guān)于這個(gè)名字其實(shí)有一個(gè)小故事在里面.金絲雀,美麗的鳥兒.她的歌聲不僅動(dòng)聽,還曾挽救過無數(shù)礦工的生命.17世紀(jì),英國(guó)礦井工人發(fā)現(xiàn),金絲雀對(duì)瓦斯這種氣體十分敏感.空氣中哪怕有極其微量的瓦斯,金絲雀也會(huì)停止歌唱;而當(dāng)瓦斯含量超過一定限度時(shí),雖然魯鈍的人類毫無察覺,金絲雀卻早已毒發(fā)身亡.當(dāng)時(shí)在采礦設(shè)備相對(duì)簡(jiǎn)陋的條件下,工人們每次下井都會(huì)帶上一只金絲雀作為"瓦斯檢測(cè)指標(biāo)",以便在危險(xiǎn)狀況下緊急撤離. 同樣的,LeakCanary這只"金絲雀"能非常敏感地幫我們發(fā)現(xiàn)內(nèi)存泄露,從而避免OOM的風(fēng)險(xiǎn).

2. 初始化

在引入LeakCanary的時(shí)候,只需要在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! 我們不需要改動(dòng)任何的代碼,就這樣,LeakCanary就已經(jīng)引入進(jìn)來了. 那我有疑問了?我們一般引入一個(gè)庫都是在Application的onCreate中初始化,它不需要在代碼中初始化,它是如何起作用的呢?

我只想到一種方案可以實(shí)現(xiàn)這個(gè),就是它在內(nèi)部定義了一個(gè)ContentProvider,然后在ContentProvider的里面進(jìn)行的初始化.

咱驗(yàn)證一下: 引入LeakCanary之后,運(yùn)行一下項(xiàng)目,然后在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對(duì)應(yīng)的是@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里面進(jìn)行的初始化.App在啟動(dòng)時(shí)會(huì)自動(dòng)初始化ContentProvider,也就自動(dòng)調(diào)用了AppWatcher.manualInstall()進(jìn)行了初始化.一開始的時(shí)候,我覺得這樣挺好的,挺優(yōu)雅,后來發(fā)現(xiàn)好多三方庫都這么干了.每個(gè)庫一個(gè)ContentProvider進(jìn)行初始化,有點(diǎn)冗余的感覺.后來Jetpack推出了App Startup,解決了這個(gè)問題,它就是基于這個(gè)原理進(jìn)行的封裝.

需要注意的是ContentProvider的onCreate執(zhí)行時(shí)機(jī)比Application的onCreate執(zhí)行時(shí)機(jī)還早.如果你想在其他時(shí)機(jī)進(jìn)行初始化優(yōu)化啟動(dòng)時(shí)間,也是可以的.只需要在app里重寫@bool/leak_canary_watcher_auto_install的值為false即可.然后手動(dòng)在合適的地方調(diào)用AppWatcher.manualInstall(application).但是LeakCanary本來就是在debug的時(shí)候用的,所以感覺優(yōu)化啟動(dòng)時(shí)間不是那么必要.

3. 監(jiān)聽泄露的時(shí)機(jī)

LeakCanary自動(dòng)檢測(cè)以下對(duì)象的泄露:

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

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

3.1 Activity

通過Application#registerActivityLifecycleCallbacks()注冊(cè)Activity生命周期監(jiān)聽,然后在onActivityDestroyed()中進(jìn)行objectWatcher.watch(activity,....)進(jìn)行檢測(cè)對(duì)象是否泄露.檢測(cè)對(duì)象是否泄露這塊后面單獨(dú)分析.

3.2 Fragment逛球、Fragment View

同樣的,檢測(cè)這2個(gè)也是需要監(jiān)聽周期,不過這次監(jiān)聽的是Fragment的生命周期,利用fragmentManager.registerFragmentLifecycleCallbacks可以實(shí)現(xiàn).Fragment是在onFragmentDestroy()中檢測(cè)Fragment對(duì)象是否泄露,Fragment View在onFragmentViewDestroyed()里面檢測(cè)Fragment View對(duì)象是否泄露.

但是,拿到這個(gè)fragmentManager的過程有點(diǎn)曲折.

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

可以看到,不同的場(chǎng)景下,取FragmentManager的方式是不同的.取FragmentManager的實(shí)現(xiàn)過程稻爬、注冊(cè)Fragment生命周期、在onFragmentDestroyed和onFragmentViewDestroyed中檢測(cè)對(duì)象是否有泄漏這一套邏輯,在不同的環(huán)境下,實(shí)現(xiàn)不同.所以把它們封裝進(jìn)不同的策略(對(duì)應(yīng)著上面3種策略)中,這就是策略模式的應(yīng)用.

因?yàn)樯厦娅@取FragmentManager需要Activity實(shí)例,所以這里還需要監(jiān)聽Activity生命周期,在onActivityCreated()中拿到Activity實(shí)例,從而拿到FragmentManager去監(jiān)聽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中還會(huì)單獨(dú)監(jiān)聽onFragmentCreated()

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

install里面實(shí)際是通過fragment和ViewModelProvider生成一個(gè)ViewModelClearedWatcher,這是一個(gè)新的ViewModel,然后在這個(gè)ViewModel的onCleared()里面檢測(cè)這個(gè)fragment里面的每個(gè)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. 監(jiān)測(cè)對(duì)象是否泄露

在講這個(gè)之前得先回顧一個(gè)知識(shí)點(diǎn),Java中的WeakReference是弱引用類型,每當(dāng)發(fā)生GC時(shí),它所持有的對(duì)象如果沒有被其他強(qiáng)引用所持有,那么它所引用的對(duì)象就會(huì)被回收,同時(shí)或者稍后的時(shí)間這個(gè)WeakReference會(huì)被入隊(duì)到ReferenceQueue中.LeakCanary中檢測(cè)內(nèi)存泄露就是基于這個(gè)原理.

/**
 * 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);
    }

}

實(shí)現(xiàn)要點(diǎn):

  1. 當(dāng)一個(gè)對(duì)象需要被回收時(shí),生成一個(gè)唯一的key,將它們封裝進(jìn)KeyedWeakReference中,并傳入自定義的ReferenceQueue
  2. 將key和KeyedWeakReference放入一個(gè)map中
  3. 過一會(huì)兒之后(默認(rèn)是5秒)主動(dòng)觸發(fā)GC,將自定義的ReferenceQueue中的KeyedWeakReference全部移除(它們所引用的對(duì)象已被回收),并同時(shí)根據(jù)這些KeyedWeakReference的key將map中的KeyedWeakReference也移除掉.
  4. 此時(shí)如果map中還有KeyedWeakReference剩余,那么就是沒有入隊(duì)的,也就是說這些KeyedWeakReference所對(duì)應(yīng)的對(duì)象還沒被回收.這是不合理的,這里就產(chǎn)生了內(nèi)存泄露.
  5. 將這些內(nèi)存泄露的對(duì)象分析引用鏈,保存數(shù)據(jù)

下面來看具體代碼:

//ObjectWatcher.kt

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

    //存入map    
    watchedObjects[key] = reference
    
    //默認(rèn)5秒之后執(zhí)行moveToRetained()檢查
    //這里是用的handler.postDelay實(shí)現(xiàn)的延遲
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
}

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

需要被回收的Activity、Fragment什么的都會(huì)走watch()這個(gè)方法這里,檢測(cè)是否有內(nèi)存泄露發(fā)生.上面這塊代碼對(duì)應(yīng)著實(shí)現(xiàn)要點(diǎn)的1-4步.接下來具體分析內(nèi)存泄露了是怎么走的

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

private fun checkRetainedObjects() {
    //比如如果是在調(diào)試,那么暫時(shí)先不dump heap,延遲20秒再判斷一下狀態(tài)

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

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

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      //1分鐘之內(nèi)才dump過,再過會(huì)兒再來
      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這個(gè)IntentService去分析heap,具體在HeapAnalyzerService#runAnalysis()
    dumpHeap(retainedReferenceCount, retry = true)
  }

HeapAnalyzerService 里調(diào)用的是 Shark 庫對(duì) heap 進(jìn)行分析狼荞,分析的結(jié)果再返回到 DefaultOnHeapAnalyzedListener.onHeapAnalyzed 進(jìn)行分析結(jié)果入庫宅粥、發(fā)送通知消息啊楚。

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. 總結(jié)

LeakCanary是一只優(yōu)雅的金絲雀,幫助我們監(jiān)測(cè)內(nèi)存泄露.本文主要分析了LeakCanary的初始化吠冤、監(jiān)聽泄露的時(shí)機(jī)、監(jiān)測(cè)某個(gè)對(duì)象泄露的過程.源碼中實(shí)現(xiàn)非常優(yōu)雅,本文中未完全展現(xiàn)出來,比較源碼太多貼上來不太雅觀.讀源碼不僅能讓我們學(xué)到新東西,而且也讓我們以后寫代碼有可以模仿的對(duì)象,甚至還可以在面試時(shí)得心應(yīng)手,一舉三得.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末特幔,一起剝皮案震驚了整個(gè)濱河市咨演,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚯斯,老刑警劉巖薄风,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拍嵌,居然都是意外死亡遭赂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門横辆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撇他,“玉大人,你說我怎么就攤上這事狈蚤±Ъ纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵脆侮,是天一觀的道長(zhǎng)锌畸。 經(jīng)常有香客問我,道長(zhǎng)靖避,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任幻捏,我火速辦了婚禮盆犁,結(jié)果婚禮上篡九,老公的妹妹穿的比我還像新娘谐岁。我一直安慰自己,他們只是感情好榛臼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布伊佃。 她就那樣靜靜地躺著,像睡著了一般讽坏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上例证,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天路呜,我揣著相機(jī)與錄音,去河邊找鬼。 笑死胀葱,一個(gè)胖子當(dāng)著我的面吹牛漠秋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抵屿,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼庆锦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了轧葛?” 一聲冷哼從身側(cè)響起搂抒,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尿扯,沒想到半個(gè)月后求晶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衷笋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年芳杏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辟宗。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爵赵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泊脐,到底是詐尸還是另有隱情空幻,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布晨抡,位于F島的核電站氛悬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耘柱。R本人自食惡果不足惜如捅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望调煎。 院中可真熱鬧镜遣,春花似錦、人聲如沸士袄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娄柳。三九已至寓辱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赤拒,已是汗流浹背秫筏。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工诱鞠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人这敬。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓航夺,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親崔涂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阳掐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容