性能優(yōu)化(三):內(nèi)存泄露檢測框架LeakCanary

金絲雀.png

LeakCanary使用只需在app中的build.gradle添加依賴

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

沒錯骗卜,一行搞定!

<provider  
   android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
   android:authorities="${applicationId}.leakcanary-installer"
   android:enabled="@bool/leak_canary_watcher_auto_install"
   android:exported="false"/>
internal sealed class AppWatcherInstaller : ContentProvider() {
  override fun onCreate(): Boolean {
      val application = context!!.applicationContext as Application
        //進行初始化
      AppWatcher.manualInstall(application)
      return true
    }
}

apk打包流程中會把這個provider合并到app下的mainfest文件中,ContentProvider的onCreate比Application的onCreate早執(zhí)行左胞,調(diào)用AppWatcher.manualInstall(application)進行初始化的寇仓。

//AppWatcher
fun manualInstall(
    application: Application,
    //5s,后面checkRetainedExecutor.execute有用到
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {
   //...
    watchersToInstall.forEach {
      it.install()
    }
  }

//初始化4個watcher
 fun appDefaultWatchers(
    application: Application,
    reachabilityWatcher: ReachabilityWatcher = objectWatcher
  ): List<InstallableWatcher> {
    return listOf(
      ActivityWatcher(application, reachabilityWatcher),
      FragmentAndViewModelWatcher(application, reachabilityWatcher),
      RootViewWatcher(reachabilityWatcher),
      ServiceWatcher(reachabilityWatcher)
    )
  }

LeakCanary會Activity烤宙、Fragment遍烦、Fragment的viewViewModel躺枕、RootViewService納入檢測服猪。

監(jiān)聽泄漏的時機

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"
        )
      }
    }

  override fun install() {
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

ActivityWatcher通過registerActivityLifecycleCallbacks監(jiān)聽Activity生命周期回調(diào),在onActivityDestroyed時拐云,調(diào)用objectWatcher.expectWeaklyReachable將Activity納入檢測

FragmentAndViewModelWatcher
兼容了O以上罢猪、AndroidX、Support慨丐,通過fragmentManager.registerFragmentLifecycleCallbacks監(jiān)聽坡脐,在onFragmentViewDestroyed與onFragmentDestroyed中調(diào)用expectWeaklyReachable納入檢測。
對于ViewModel房揭,在AndroidXFragmentDestroyWatcher里還會額外監(jiān)聽

ViewModelClearedWatcher.install(activity, reachabilityWatcher)

反射獲取ViewModelStore的mMap备闲, 在ViewModelClearedWatcher的onCleared中調(diào)用expectWeaklyReachable將ViewModel納入檢測。

RootViewWatcher
通過反射獲取WindowManagerGlobal中的mViews捅暴,再通過addOnAttachStateChangeListener監(jiān)聽rootView恬砂,在onViewDetachedFromWindow時執(zhí)行expectWeaklyReachable納入檢測。

ServiceWatcher
1.反射獲取ActivityThread中的mServices(app中全部Service的一個Map)蓬痒。
2.反射獲取名為H的Handler(Android消息機制中轉(zhuǎn)中心)泻骤。
3.替換H的mCallBack實現(xiàn),當(dāng)消息為STOP_SERVICE時梧奢,便從mServices取出該消息對應(yīng)的Service作為待檢測Service引用狱掂。
4.Hook AMS,通過動態(tài)代理修改它的serviceDoneExecuting方法亲轨,在onServiceDestroyed時執(zhí)行expectWeaklyReachable納入檢測趋惨。

如何檢測內(nèi)存泄漏?

原理:Java中的WeakReference表示弱引用惦蚊,當(dāng)GC時器虾,它所持有的對象如果沒有被其它強引用持有讯嫂,那么它所引用的對象就會被回收,這個WeakReference會被加入到關(guān)聯(lián)的ReferenceQueue兆沙。

最終都是調(diào)用了expectWeaklyReachable納入檢測

//核心代碼片段 ObjectWatcher.kt

private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

private val queue = ReferenceQueue<Any>()

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
    if (!isEnabled()) {
      return
    }
    //遍歷queue欧芽,從watchedObjects刪除已回收的對象
    removeWeaklyReachableObjects()
    //生成一個uuid作為key
    val key = UUID.randomUUID()
    .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    //構(gòu)建當(dāng)前引用的弱引用對象,并關(guān)聯(lián)引用隊列queue
    val reference =
    KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
      //將構(gòu)建的弱引用存入watchedObjects
    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      //Handler.postDelayed 實現(xiàn)延遲5s執(zhí)行
      moveToRetained(key)
    }
}


@Synchronized private fun moveToRetained(key: String) {
    // 再檢查一遍是否已經(jīng)回收
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      //說明可能存在內(nèi)存泄漏
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
}


private fun removeWeaklyReachableObjects() {
    var ref: KeyedWeakReference?
    do {
      //隊列queue中的對象都是會被GC的
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        //說明釋放了葛圃,從watchedObjects刪除被回收的對象(移除watchedObjects集合中被GC的ref對象千扔,剩下的就可能是泄漏的對象)
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }

最后檢查對象沒喲被回收的話,調(diào)用onObjectRetained()方法

    onObjectRetained
—>InternalLeakCanary.scheduleRetainedObjectCheck()
—>HeapDumpTrigger.scheduleRetainedObjectCheck 
—>HeapDumpTrigger.scheduleRetainedObjectCheck    
private fun checkRetainedObjects() {
    //...
    val config = configProvider()
    
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      //調(diào)用Runtime.getRuntime().gc()執(zhí)行一次GC装悲,再來看還剩下多少對象未被回收
      //GC后Thread.sleep(100)確保對象被GC 等回收的引用入隊
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
        //當(dāng)前泄漏實例<5昏鹃,不進行heap dump
    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過,等會再來
      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"
    //最終調(diào)用dumpHeap
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    )
  }
 private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    //調(diào)用AndroidHeapDumper的dumpHeap()方法—>Debug.dumpHprofData(heapDumpFile.absolutePath)
    when (val heapDumpResult = heapDumper.dumpHeap()) {
      is HeapDump -> {
        lastDisplayedRetainedObjectCount = 0
        lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
        //清除這次dump之前的引用
        objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
        //通過HeapAnalyzerService 去分析 heap ( 使用Shark庫對heap進行分析)
        HeapAnalyzerService.runAnalysis(
          context = application,
          heapDumpFile = heapDumpResult.file,
          heapDumpDurationMillis = heapDumpResult.durationMillis,
          heapDumpReason = reason
        )
      }
    }
  }

總結(jié)

1.如何初始化
apk打包流程中會把AppWatcherInstaller這個provider合并到app下的mainfest文件中诀诊,ContentProvider的onCreate比Application的onCreate早執(zhí)行,調(diào)用AppWatcher.manualInstall(application)進行初始化的

2.檢測時機

對象 如何獲取引用 何時納入檢測
Activity ActivityLifecycleCallbacks回調(diào) onActivityDestroyed
Fragment FragmentLifecycleCallbacks回調(diào) onFragmentDestroyed
Fragment中的View FragmentLifecycleCallbacks回調(diào) onFragmentViewDestroyed
ViewModel 反射獲取ViewModelStore的mMap ViewModel的onCleared
RootView 反射獲取WindowManagerGlobal中的mViews onViewDetachedFromWindow
Service Hook H的mCallback實現(xiàn)阅嘶,當(dāng)消息為STOP_SERVICE時属瓣,從ActivityThread中的mServices獲取 onServiceDestroyed

3.檢測原理
當(dāng)jvm進行垃圾回收時,無論內(nèi)存是否充足讯柔,如果該對象只有弱引用存在抡蛙,那么就會被垃圾回收器回收,同時該引用會被加入到關(guān)聯(lián)的ReferenceQueue魂迄。

LeakCanary利用弱引用的特性粗截,獲取當(dāng)前引用,構(gòu)建弱引用對象KeyedWeakReference并關(guān)聯(lián)一個ReferenceQueue捣炬,保存到watchedObjects中熊昌。GC后,通過key刪除已經(jīng)回收的對象湿酸,剩下的對象存在泄漏嫌疑婿屹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市推溃,隨后出現(xiàn)的幾起案子昂利,更是在濱河造成了極大的恐慌,老刑警劉巖铁坎,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜂奸,死亡現(xiàn)場離奇詭異,居然都是意外死亡硬萍,警方通過查閱死者的電腦和手機扩所,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來襟铭,“玉大人碌奉,你說我怎么就攤上這事短曾。” “怎么了赐劣?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵嫉拐,是天一觀的道長。 經(jīng)常有香客問我魁兼,道長婉徘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任咐汞,我火速辦了婚禮盖呼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘化撕。我一直安慰自己几晤,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布植阴。 她就那樣靜靜地躺著蟹瘾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掠手。 梳的紋絲不亂的頭發(fā)上憾朴,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音喷鸽,去河邊找鬼众雷。 笑死,一個胖子當(dāng)著我的面吹牛做祝,可吹牛的內(nèi)容都是我干的砾省。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剖淀,長吁一口氣:“原來是場噩夢啊……” “哼纯蛾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纵隔,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤翻诉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捌刮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碰煌,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年绅作,在試婚紗的時候發(fā)現(xiàn)自己被綠了芦圾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡俄认,死狀恐怖个少,靈堂內(nèi)的尸體忽然破棺而出洪乍,到底是詐尸還是另有隱情,我是刑警寧澤夜焦,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布壳澳,位于F島的核電站,受9級特大地震影響茫经,放射性物質(zhì)發(fā)生泄漏巷波。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一卸伞、第九天 我趴在偏房一處隱蔽的房頂上張望抹镊。 院中可真熱鬧,春花似錦荤傲、人聲如沸垮耳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氨菇。三九已至,卻和暖如春妓湘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乌询。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工榜贴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妹田。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓唬党,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鬼佣。 傳聞我的和親對象是個殘疾皇子驶拱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

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