Android開發者,是時候瞭解LeakCanary2.0了

Hi,各位朋友們,笨鳥又跟大家見面啦。之後的一兩個月裏,小笨鳥將會聚焦於Android性能優化這個方面,推出一系列的文章。做爲android進階知識,性能優化不論是在社招面試仍是在平常工做中都是至關實用的知識,而且也是區分中級和高級程序員的試金石。小笨鳥就會以不一樣的專題來進行講解,但願你們喜歡,若是想了解更多的話,歡迎關注我一塊兒學習。html

今天咱們先學習內存優化中的一個小知識點,就是內存泄露的檢測和解決。固然,如何解決是大多數中級工程師都要去學習的東西,網上也有大量的資料,因此我這裏不會詳解。而是主要着眼於內存泄露的檢測。Square公司出品的大名鼎鼎的LeakCanary,就是業界知名的內存泄露檢測的利器。java

閱讀本篇文章,預計須要20分鐘,你將會學習到:android

  • LeakCanary檢測內存泄露的原理
  • 使用ContentProvider進行三方庫初始化的方法

原理概述

關於LeakCanary的原理,官網上已經給出了詳細的解釋。翻譯過來就是: 1.LeakCanary使用ObjectWatcher來監控Android的生命週期。當Activity和Fragment被destroy之後,這些引用被傳給ObjectWatcher以WeakReference的形式引用着。若是gc完5秒鐘之後這些引用尚未被清除掉,那就是內存泄露了。
2.當被泄露掉的對象達到一個閾值,LeakCanary就會把java的堆棧信息dump到.hprof文件中。
3.LeakCanary用Shark庫來解析.hprof文件,找到沒法被清理的引用的引用棧,而後再根據對Android系統的知識來斷定是哪一個實例致使的泄露
4.經過泄露信息,LeakCanary會將一條完整的引用鏈縮減到一個小的引用鏈,其他的由於這個小的引用鏈致使的泄露鏈都會被聚合在一塊兒。git

經過官網的介紹,咱們很容易就抓住了學習LeakCanary這個庫的重點:程序員

  • LeakCanary是如何使用ObjectWatcher 監控生命週期的?
  • LeakCanary如何dump和分析.hprof文件的?

看官方原理老是感受不過癮,下面咱們從代碼層面上來分析。本文基於LeakCanary 2.0 beta版。github

基本使用

LeakCanary的使用至關的簡單。只須要在module的build.gradle添加一行依賴,代碼侵入少。面試

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-3'
}
複製代碼

就這樣,應用很是簡單就接入了LeakCanary內存檢測功能。固然還有一些更高級的用法,好比更改自定義config,增長監控項等,你們能夠參考官網編程

源碼分析

1. 初始化

和以前的1.x版本相比,2.0甚至都不須要再在Application裏面增長install的代碼。可能不少的同窗都會疑惑,LeakCanary是如何插入本身的初始化代碼的呢? 其實這裏LeakCanary是使用了ContentProvider來進行初始化。我以前在介紹Android插件化系列三:技術流派和四大組件支持的時候曾經介紹過ContentProvider的特色,即在打包的過程當中來自不一樣module的ContentProvider最後都會merge到一個文件中,啓動app的時候ContentProvider是自動安裝,而且安裝會比Application的onCreate還早。LeakCanary就是依據這個原理進行的設計。具體能夠參考【譯】你的Android庫是否還在Application中初始化?性能優化

咱們能夠查看LeakCanary源碼,發現它在leakcanary-object-watcher-androidAndroidManifest.xml中有一個ContentProvider。bash

<provider
        android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
        android:authorities="${applicationId}.leakcanary-installer"
        android:exported="false"/>
複製代碼

而後咱們查看AppWatcherInstaller的代碼,發現內部是使用InternalAppWatcher進行的install。

// AppWatcherInstaller
override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    InternalAppWatcher.install(application)
    return true
}

  // InternalAppWatcher
fun install(application: Application) {
    // 省略部分代碼
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    InternalAppWatcher.application = application

    val configProvider = { AppWatcher.config }
    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
    onAppWatcherInstalled(application)
}
複製代碼

能夠看到這裏主要把Activity和Fragment區分了開來,而後分別進行註冊。Activity的生命週期監聽是藉助於Application.ActivityLifecycleCallbacks。

private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(activity)
        }
      }
    }

application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
複製代碼

而Fragment的生命週期監聽是藉助了Activity的ActivityLifecycleCallbacks生命週期回調,當Activity建立的時候去調用FragmentManager.registerFragmentLifecycleCallbacks方法註冊Fragment的生命週期監聽。

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

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      if (configProvider().watchFragments) {
        objectWatcher.watch(fragment)
      }
    }
  }
複製代碼

最終,Activity和Fragment都將本身的引用傳入了ObjectWatcher.watch()進行監控。從這裏開始進入到LeakCanary的引用監測邏輯。

題外話:LeakCanary 2.0版本和1.0版本相比,增長了Fragment的生命週期監聽,每一個類的職責也更加清晰。可是我我的以爲使用 (Activty)->Unit 這種lambda表達式做爲類的寫法不是很優雅,倒不如面向接口編程。徹底能夠設計成ActivityWatcher和FragmentWatcher都繼承自某個接口,這樣也方便後續擴展。

2. 引用監控

2.1 引用和GC

1.引用
首先咱們先介紹一點準備知識。你們都知道,java中存在四種引用:

  • 強引用:垃圾回收器毫不會回收它,當內存空間不足,Java虛擬機寧願拋出OOM
  • 軟引用:只有在內存不足的時候JVM纔會回收僅有軟引用指向的對象所佔的空間
  • 弱引用:當JVM進行垃圾回收時,不管內存是否充足,都會回收僅被弱引用關聯的對象。
  • 虛引用:和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。

一個對象在被gc的時候,若是發現還有軟引用(或弱引用,或虛引用)指向它,就會在回收對象以前,把這個引用加入到與之關聯的引用隊列(ReferenceQueue)中去。若是一個軟引用(或弱引用,或虛引用)對象自己在引用隊列中,就說明該引用對象所指向的對象被回收了。

當軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那麼這個引用對象自己就沒有價值了,若是程序中存在大量的這類對象(注意,咱們建立的軟引用、弱引用、虛引用對象自己是個強引用,不會自動被gc回收),就會浪費內存。所以咱們這就能夠手動回收位於引用隊列中的引用對象自己。

好比咱們常常看到這種用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list);
複製代碼

還有也有這樣一種用法

WeakReference<ArrayList> weakReference = new WeakReference<ArrayList>(list, new ReferenceQueue<WeakReference<ArrayList>>());
複製代碼

這樣就能夠把對象和ReferenceQueue關聯起來,進行對象是否gc的判斷了。另外咱們從弱引用的特徵中看到,弱引用是不會影響到這個對象是否被gc的,很適合用來監控對象的gc狀況。

2.GC
java中有兩種手動調用GC的方式。

System.gc();
// 或者
Runtime.getRuntime().gc();
複製代碼

2.2 監控

咱們在第一節中提到,Activity和Fragment都依賴於響應的LifecycleCallback來回調銷燬信息,而後調用了ObjectWatcher.watch添加了銷燬後的監控。接下來咱們看ObjectWatcher.watch作了什麼操做。

@Synchronized fun watch( watchedObject: Any, name: String ) {
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID().toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

  private fun removeWeaklyReachableObjects() {
    // 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.
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }
複製代碼

這裏咱們看到,有一個存儲着KeyedWeakReference的ReferenceQueue對象。在每次增長watch object的時候,都會去把已經處於ReferenceQueue中的對象給從監控對象的map即watchObjects中清理掉,由於這些對象都已經被回收了。而後再去生成一個KeyedWeakReference,這個對象就是一個持有了key和監測開始時間的WeakReference對象。 最後再去調用moveToRetained,至關於記錄和回調給監控方這個對象正式開始監測的時間。

那麼咱們如今已經拿到了須要監控的對象了,可是又是怎麼去判斷這個對象已經內存泄露的呢?這就要繼續往下面看。咱們主要到前面在講解InternalAppWatcher的install方法的時候,除了install了Activity和Fragment的檢測器,還調用了onAppWatcherInstalled(application)方法,看代碼發現這個方法就是InternalLeakCanary的invoke方法。

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

    AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

    val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider)

    val gcTrigger = GcTrigger.Default

    val configProvider = { LeakCanary.config }

    val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
    handlerThread.start()
    val backgroundHandler = Handler(handlerThread.looper)

    heapDumpTrigger = HeapDumpTrigger(
        application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
        configProvider
    )
  }

  override fun onObjectRetained() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onObjectRetained()
    }
  }
複製代碼

咱們看到首先是初始化了heapDumper,gcTrigger,heapDumpTrigger等對象用於gc和heapDump,同時還實現了OnObjectRetainedListener,並把本身添加到了上面的onObjectRetainedListeners中,以便每一個對象moveToRetained的時候,InternalLeakCanary都能獲取到onObjectRetained()的回調,回調裏就只是回調了heapDumpTrigger.onObjectRetained()方法。看來都是依賴於HeapDumpTrigger這個類。

HeapDumpTrigger主要的處理邏輯都在checkRetainedObjects方法中。

private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()  // 觸發一次GC操做,只保留不能被回收的對象
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }

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

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      showRetainedCountWithDebuggerAttached(retainedReferenceCount)
      scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
      return
    }

    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    dismissRetainedCountNotification()
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
      scheduleRetainedObjectCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
      showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
      return
    }
    lastDisplayedRetainedObjectCount = 0
    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)

    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }
複製代碼

那麼HeapDumpTrigger具體作了些啥呢?我理了一下主要是下面幾個功能:

  • 後臺線程輪詢當前還存活着的對象
  • 若是存活的對象大於0,那就觸發一次GC操做,回收掉沒有泄露的對象
  • GC完後,仍然存活着的對象數和預約的對象數相比較,若是多了就調用heapDumper.dumpHeap()方法把對象dump成文件,並交給HeapAnalyzerService去分析
  • 根據存活狀況展現通知

2.3 總結

看到了這裏,咱們應該腦海中有概念了。Activity和Fragment經過註冊系統的監聽在onDestroy的時候把本身的引用放入ObjectWatcher進行監測,監測主要是經過HeapDumpTrigger類輪詢進行,主要是調用AndroidHeapDumper來dump出文件來,而後依賴於HeapAnalyzerService來進行分析。後面一小節,咱們將會聚焦於對象dump操做和HeapAnalyzerService的分析過程。

3. dump對象及分析

3.1 dump對象

hprof是JDK提供的一種JVM TI Agent native工具。JVM TI,全拼是JVM Tool interface,是JVM提供的一套標準的C/C++編程接口,是實現Debugger、Profiler、Monitor、Thread Analyser等工具的統一基礎,在主流Java虛擬機中都有實現。hprof工具事實上也是實現了這套接口,能夠認爲是一套簡單的profiler agent工具。咱們在新知周推:10.8-10.14(啓動篇)中也提到過,能夠參考其中美團的文章

用過Android Studio Profiler工具的同窗對hprof文件都不會陌生,當咱們使用Memory Profiler工具的Dump Java heap圖標的時候,profiler工具就會去捕獲你的內存分配狀況。可是捕獲之後,只有在Memory Profiler正在運行的時候咱們才能查看,那麼咱們要怎麼樣去保存當時的內存使用狀況呢,又或者我想用別的工具來分析堆分配狀況呢,這時候hprof文件就派上用場了。Android Studio能夠把這些對象給export到hprof文件中去。

LeakCanary也是使用的hprof文件進行對象存儲。hprof文件比較簡單,總體按照 前置信息 + 記錄表的格式來組織的。可是記錄的種類至關之多。具體種類能夠查看HPROF Agent

同時,android中也提供了一個簡便的方法Debug.dumpHprofData(filePath)能夠把對象dump到指定路徑下的hprof文件中。LeakCanary使用使用Shark庫來解析Hprof文件中的各類record,比較高效,使用Shark中的HprofReader和HprofWriter來進行讀寫解析,獲取咱們須要的信息。你們能夠關注一些比較重要的,好比:

  • Class Dump
  • Instance Dump
  • Object Array Dump
  • Primitive Array Dump

dump具體的代碼在AndroidHeapDumper類中。HprofReader和HprofWriter過於複雜,有興趣的直接查看源碼吧

override fun dumpHeap(): File? {
    val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null

    return try {
      Debug.dumpHprofData(heapDumpFile.absolutePath)
      if (heapDumpFile.length() == 0L) {
        null
      } else {
        heapDumpFile
      }
    } catch (e: Exception) {
      null
    } finally {
    }
  }
複製代碼

3.2 對象分析

前面咱們已經分析到了,HeapDumpTrigger主要是依賴於HeapAnalyzerService進行分析。那麼這個HeapAnalyzerService究竟有什麼玄機?讓咱們繼續往下面看。能夠看到HeapAnalyzerService實際上是一個ForegroundService。在接收到分析的Intent後就會調用HeapAnalyzer的analyze方法。因此最終進行分析的地方就是HeapAnalyzer的analyze方法。

核心代碼以下

try {
      listener.onAnalysisProgress(PARSING_HEAP_DUMP)
      Hprof.open(heapDumpFile)
          .use { hprof ->
           // 1.生成graph
            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
            // 2.尋找Leak
            val findLeakInput = FindLeakInput(
                graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors
            )
            val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks()
            listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
            return HeapAnalysisSuccess(
                heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
                applicationLeaks, libraryLeaks
            )
          }
    } catch (exception: Throwable) {
      listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
      return HeapAnalysisFailure(
          heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
          HeapAnalysisException(exception)
      )
    }
  }
複製代碼

這段代碼中涉及到了專爲LeakCanary設計的Shark庫的用法,在這裏就很少解釋了。大概介紹一下每一步的做用:

  • 首先調用HprofHeapGraph.indexHprof方法,這個方法會把dump出來的各類實例instance,Class類對象和Array對象等都創建起查詢的索引,以record的id做爲key,把須要的信息都存儲在Map中便於後續取用
  • 調用findLeakInput.findLeak方法,這個方法會從GC Root開始查詢,找到最短的一條致使泄露的引用鏈,而後再根據這條引用鏈構建出LeakTrace。
  • 把查詢出來的LeakTrace對外展現

總結

本篇文章分析了LeakCanary檢測內存泄露的思路和一些代碼的設計思想,可是限於篇幅不能面面俱到。接下來咱們回答一下文章開頭提出的問題。

1.LeakCanary是如何使用ObjectWatcher 監控生命週期的?
LeakCanary使用了Application的ActivityLifecycleCallbacks和FragmentManager的FragmentLifecycleCallbacks方法進行Activity和Fragment的生命週期檢測,當Activity和Fragment被回調onDestroy之後就會被ObjectWatcher生成KeyedReference來檢測,而後藉助HeapDumpTrigger的輪詢和觸發gc的操做找到彈出提醒的時機。

2.LeakCanary如何dump和分析.hprof文件的?
使用Android平臺自帶的Debug.dumpHprofData方法獲取到hprof文件,使用自建的Shark庫進行解析,獲取到LeakTrace

參考

LeakCanary官網

HPROF Agent

【譯】你的Android庫是否還在Application中初始化?

我是Android笨鳥之旅,笨鳥也要有向上飛的心,我在這裏陪你一塊兒慢慢變強。

相關文章
相關標籤/搜索