讀源碼-LeakCanary2.4解析

本文基於LeakCanary版本:
'com.squareup.leakcanary:leakcanary-android:2.4'java

1-基本原理

  在開始LeakCanary源碼分析前,先來了解下RefercenceReferenceQueue,它們是LeakCanary實現內存泄漏監聽的核心。android

1.1-Reference & ReferenceQueue

  (1) Reference即引用,是一個泛型抽象類。Android中的SoftReference(軟引用)、WeakReference(弱引用)、PhantomReference(虛引用)都是繼承自Reference。來看下Reference的幾個主要成員變量。web

public abstract class Reference<T> {
  // 引用對象,被回收時置null  volatile T referent;  //保存即將被回收的reference對象  final ReferenceQueue<? super T> queue;   //在Enqueued狀態下即引用加入隊列時,指向下一個待處理Reference對象,默認爲null  Reference queueNext;  //在Pending狀態下,待入列引用,默認爲null  Reference<?> pendingNext; } 複製代碼

  Reference有四種狀態:ActivePendingEnqueuedInactive。聲明的時候默認Active狀態,四種狀態的切換關係:數據結構

  • queue不爲空時
    -->GC回收referent時,將referent置爲null,並將該Reference對象放入clear隊列,狀態變爲 Pending,此時queueNext爲空,pendingNext不爲空。
    -->GC會喚醒ReferenceQueueDaemon線程處理clear隊列,將Reference對象放入queue隊列,狀態變爲 Enqueued,此時queueNext不爲空,pendingNext爲該Reference)。
    -->當queue調用poll()將該Reference對象出列後,狀態變爲 Inactive,此時queueNext爲一個新建虛引用(虛引用get返回null),pendingNext爲該Reference
  • queue爲空
    -->GC回收referent時,將referent置爲null,狀態變爲 Inactive,此時queueNext、pendingNext都爲null

  (2) ReferenceQueue則是一個單向鏈表實現的隊列數據結構,存儲的是Reference對象。包含了入列enqueue、出列poll和移除remove操做app

1.2-對象回收監聽

  Reference配合ReferenceQueue就能夠實現對象回收監聽了,先經過一個示例來看看是怎麼實現的。dom

//建立一個引用隊列
ReferenceQueue queue = new ReferenceQueue(); //建立弱引用,並關聯引用隊列queue WeakReference reference = new WeakReference(new Object(),queue); System.out.println(reference); System.gc(); //當reference被成功回收後,能夠從queue中獲取到該引用 System.out.println(queue.remove()); 複製代碼

  示例中的對象固然是能夠正常回收的,因此回收後能夠在關聯的引用隊列queue中獲取到該引用。反之,若某個應該被回收的對象,GC結束後在queue中未找到該引用,則代表該引用存在內存泄漏風險,這也就是LeakCanary的基本原理了。編輯器

2-LeakCanary源碼分析

2.1-初始化

  2.0以前的版本接入過程除了在build.gradle中引入項目外,還須要調用LeakCanary.install(this);來進行初始化工做。在2.0以後的版本只須要在build.gradle引入項目就完事了。那麼問題來了:2.0以後的版本初始化工做是在哪裏完成的呢?
  找了許久,終於在項目工程:leakcanary-object-watcher-android的manifest文件中發現了祕密:ide

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

  這裏註冊了一個繼承自ContentProviderAppWatcherInstaller。咱們知道在app啓動時,會先調用註冊的ContentProvider的onCreate完成初始化,在AppWatcherInstaller.onCreate中果真找到了熟悉的install方法:函數

override fun onCreate(): Boolean {
 val application = context!!.applicationContext as Application  AppWatcher.manualInstall(application)  return true } 複製代碼

  調用鏈:AppWatcher.manualInstall-->InternalAppWatcher.install。具體的初始化邏輯是在InternalAppWatcher,來看源碼:oop

fun install(application: Application) {
 //確保在主線程,不然拋出UnsupportedOperationException異常  checkMainThread()  //確保application已賦值,application是lateinit修飾的延遲初始化變量  if (this::application.isInitialized) {  return  }  //leakcanary日誌初始化  SharkLog.logger = DefaultCanaryLog()  InternalAppWatcher.application = application  //日誌配置初始化  val configProvider = { AppWatcher.config }  //Activity內存泄漏監聽器初始化  ActivityDestroyWatcher.install(application, objectWatcher, configProvider)  //Fragment內存泄漏監聽器初始化  FragmentDestroyWatcher.install(application, objectWatcher, configProvider)  //註冊內存泄漏事件回調  onAppWatcherInstalled(application) } 複製代碼

  ps:ContentProvider的核心方法CURD在AppWatcherInstaller都是空實現,只用到了onCreate。原來ContentProvider還能夠這麼玩,新姿式get。須要注意的是ContentProvider.onCreate調用時機介於Application的attachBaseContextonCreate之間,因此不能依賴以後初始化的其餘SDK。

2.2-Activity監聽

  在前面初始過程當中,分別建立了針對Activity及Fragment的監聽器。咱們這裏以Activity監聽爲例進行分析,Fragment監聽除了生命週期監聽方式不一樣外後面的流程都是同樣的。

companion object {
 fun install(  application: Application,  objectWatcher: ObjectWatcher,  configProvider: () -> Config  ) {  //實例化ActivityDestroyWatcher  val activityDestroyWatcher =  ActivityDestroyWatcher(objectWatcher, configProvider)  //註冊ActivityLifecycle監聽  application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)  } } 複製代碼

  registerActivityLifecycleCallbacks是Android Application的一個方法,註冊了該方法,能夠經過回調獲取app中每個Activity的生命週期變化。再來看看ActivityDestroyWatcher對生命週期回調的處理:

private val lifecycleCallbacks =
 object : Application.ActivityLifecycleCallbacks by noOpDelegate() {  override fun onActivityDestroyed(activity: Activity) {  if (configProvider().watchActivities) {  objectWatcher.watch(  activity, "${activity::class.java.name} received Activity#onDestroy() callback"  )  }  } } 複製代碼

  ps:ActivityLifecycleCallbacks生命週期回調有那麼多,爲何只用重寫其中一個?關鍵在於by noOpDelegate(),經過類委託機制將其餘回調實現都交給noOpDelegate,而noOpDelegate是一個空實現的動態代理。新姿式get+1,在遇到只須要實現接口的部分方法時,就能夠這麼玩了,其餘方法實現都委託給空實現代理類就行了。
  接着看監聽到Activity onDestroy後的處理:

private val lifecycleCallbacks =
 object : Application.ActivityLifecycleCallbacks by noOpDelegate() {  override fun onActivityDestroyed(activity: Activity) {  if (configProvider().watchActivities) {  objectWatcher.watch(  activity, "${activity::class.java.name} received Activity#onDestroy() callback"  )  }  } } 複製代碼

  經過ObjectWatcher來監聽該Activity,即認爲該Activity實例應該被銷燬。若是不能正常銷燬則代表存在內存泄漏。

@Synchronized fun watch(  watchedObject: Any,  description: String  ) {  if (!isEnabled()) {  return  }  //@1.清空queue,即移除以前已回收的引用  removeWeaklyReachableObjects()  //生成UUID  val key = UUID.randomUUID()  .toString()  //記錄當前時間  val watchUptimeMillis = clock.uptimeMillis()  //將當前Activity對象封裝成KeyedWeakReference,並關聯引用隊列queue  //KeyedWeakReference繼承自WeakReference,封裝了用於監聽對象的輔助信息  val reference =  KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)  //輸出日誌  SharkLog.d {  "Watching " +  (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +  (if (description.isNotEmpty()) " ($description)" else "") +  " with key $key"  }  //將弱引用reference存入監聽列表watchedObjects  watchedObjects[key] = reference  //@2.進行一次後臺檢查任務,判斷引用對象是否未被回收  checkRetainedExecutor.execute {  moveToRetained(key)  } } 複製代碼

@1.清空queue,即移除以前已回收的引用。

  這個方法很重要,第一次調用是清除以前的已回收對象,後面還會再次調用該方法判斷引用是否正常回收。
  這裏涉及到的兩個重要變量:

  • queue 即引用隊列ReferenceQueue
  • watchedObjects 全部監聽Reference對象的map,key爲引用對象對應的UUID,value爲Reference對象
private fun removeWeaklyReachableObjects() {
 var ref: KeyedWeakReference?  do {  //遍歷引用隊列  ref = queue.poll() as KeyedWeakReference?  //將引用隊列中的Reference對象從監聽列表watchedObjects中移除  if (ref != null) {  watchedObjects.remove(ref.key)  }  } while (ref != null) } 複製代碼

@2.進行一次後臺檢查任務moveToRetained,5秒後判斷引用對象是否未被回收。

該任務是延遲5s後執行的

private val checkRetainedExecutor = Executor {
 //val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5),  mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis) } 複製代碼
@Synchronized private fun moveToRetained(key: String) {
 //遍歷引用隊列,並將引用隊列中的引用從監聽列表watchedObjects中移除  removeWeaklyReachableObjects()  //若對象未能成功移除,則代表引用對象可能存在內存泄漏  val retainedRef = watchedObjects[key]  if (retainedRef != null) {  retainedRef.retainedUptimeMillis = clock.uptimeMillis()  //@3.onObjectRetainedListeners內存泄漏事件回調  onObjectRetainedListeners.forEach { it.onObjectRetained() }  } } 複製代碼

  在這裏理一下moveToRetained的處理邏輯:

  • 正常狀況:Activity對象被GC回收掉進入引用隊列 queue,經過 removeWeaklyReachableObjects方法遍歷 queue獲取該引用對象後,將其從監聽列表 watchedObjects中移除。因此 watchedObjects[key]也就沒法獲取到引用對象了。
  • 異常狀況:Activity對象onDestroy後未能被GC回收掉,因此在引用隊列 queue中也就找不到該對象,也就是說監聽列表 watchedObjects中該對象沒有被刪掉。經過 watchedObjects[key]能夠拿到該引用對象,便可以判斷該引用對象存在內存泄漏問題。

@3.onObjectRetainedListeners內存泄漏事件回調

  發現內存泄漏對象後會調用onObjectRetainedListeners監聽回調,進行後續處理。那麼這個onObjectRetainedListeners是在哪裏實現的呢?
  在前面InternalAppWatcher.install初始化時,InternalAppWatcher的初始化方法onAppWatcherInstalled()中初始化了該監聽。

init {
 val internalLeakCanary = try {  val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")  leakCanaryListener.getDeclaredField("INSTANCE")  .get(null)  } catch (ignored: Throwable) {  NoLeakCanary  }  @kotlin.Suppress("UNCHECKED_CAST")  onAppWatcherInstalled = internalLeakCanary as (Application) -> Unit } 複製代碼

  咱們發現這裏經過反射獲取InternalLeakCanary.INSTANCE單列對象,這個類位於另外一個包leakcanary-android-core,因此用了反射。因爲InternalLeakCanary是一個函數對象,onAppWatcherInstalled()對應的調用方法爲invoke()來完成監聽註冊。

override fun invoke(application: Application) {
 _application = application  //檢查是否debug構建模式  checkRunningInDebuggableBuild()  //註冊監聽  AppWatcher.objectWatcher.addOnObjectRetainedListener(this)  //建立AndroidHeapDumper對象,用於虛擬機dump hprof產生內存快照文件  val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))  //GcTrigger經過Runtime.getRuntime().gc()觸發GC  val gcTrigger = GcTrigger.Default  val configProvider = { LeakCanary.config }  //建立子線程及對應looper  val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)  handlerThread.start()  val backgroundHandler = Handler(handlerThread.looper)  //HeapDumpTrigger監聽註冊  heapDumpTrigger = HeapDumpTrigger(  application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,  configProvider  )  //註冊應用可見監聽  application.registerVisibilityListener { applicationVisible ->  this.applicationVisible = applicationVisible  heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)  }  registerResumedActivityListener(application)  addDynamicShortcut(application)   disableDumpHeapInTests() } 複製代碼

  當ObjectWatcher中moveToRetained發現未回收對象後,經過回調onObjectRetained()處理時,調用的就是這裏註冊的HeapDumpTrigger.onObjectRetained()。處理調用鏈較長,直接看關鍵方法:
-->onObjectRetained-->scheduleRetainedObjectCheck-->checkRetainedObjects

private fun checkRetainedObjects(reason: String) {
 ...//代碼省略  //監聽器中未回收對象個數  var retainedReferenceCount = objectWatcher.retainedObjectCount  //執行一次GC,再更新未回收對象個數  if (retainedReferenceCount > 0) {  gcTrigger.runGc()  retainedReferenceCount = objectWatcher.retainedObjectCount  }  //若對象個數未達到閾值5,返回  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return   ...//代碼省略,60s內只會執行一次   //核心方法,獲取內存快照  dumpHeap(retainedReferenceCount, retry = true) } 複製代碼
private fun dumpHeap(  retainedReferenceCount: Int,  retry: Boolean  ) {  ...//代碼省略   //獲取當前內存快照hprof文件  val heapDumpFile = heapDumper.dumpHeap()  ...//省略hprof獲取失敗處理  lastDisplayedRetainedObjectCount = 0  lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()  //清理以前註冊的監聽  objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)  //開啓hprof分析Service,解析hprof文件生成報告  HeapAnalyzerService.runAnalysis(application, heapDumpFile) } 複製代碼

2.3-hprof文件解析

  在上面講到的內存泄漏回調處理中,生成了hprof文件,並開啓一個服務來解析該文件。調用鏈:HeapAnalyzerService.analyzeHeap-->HeapAnalyzer.analyze。該方法實現瞭解析hprof文件找到內存泄漏對象,並計算對象到GC roots的最短路徑,輸出報告。

fun analyze(.../*參數省略*/): HeapAnalysis {
 ...//代碼省略   return try {  //PARSING_HEAP_DUMP解析狀態回調  listener.onAnalysisProgress(PARSING_HEAP_DUMP)  //開始解析hprof文件  Hprof.open(heapDumpFile)  .use { hprof ->  //從文件中解析獲取對象關係圖結構graph  //並獲取圖中的全部GC roots根節點  val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)  //建立FindLeakInput對象  //@4.查找內存泄漏對象  val helpers =  FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)  helpers.analyzeGraph(  metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime  )  }  } catch (exception: Throwable) {  ...//省略解析異常處理  } } 複製代碼

@4.查找內存泄漏對象

private fun FindLeakInput.analyzeGraph(.../*參數省略*/): HeapAnalysisSuccess {
 ...//代碼省略  //經過過濾graph中的KeyedWeakReference類型對象來  //找到對應的內存泄漏對象  val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)  //@5.計算內存泄漏對象到GC roots的路徑  val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds)  //輸出最終hprof分析結果  return HeapAnalysisSuccess(.../*參數省略*/) } 複製代碼

@5.計算內存泄漏對象到GC roots的路徑

private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
 val pathFinder = PathFinder(graph, listener, referenceMatchers)  //計算並獲取目標對象到GC roots的最短路徑  val pathFindingResults =  pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)   SharkLog.d { "Found ${leakingObjectIds.size} retained objects" }  //將這些內存泄漏對象的最短路徑合併成樹結構返回。  return buildLeakTraces(pathFindingResults) } 複製代碼

  最終在可視化界面中將hprof分析結果HeapAnalysisSuccess展現出來:

總結

  最後來總結下LeakCanary內存泄漏分析過程吧(Activity):

  • (1)註冊監聽Activity生命週期onDestroy事件
  • (2)在Activity onDestroy事件回調中建立 KeyedWeakReference對象,並關聯 ReferenceQueue
  • (3)延時5秒檢查目標對象是否回收
  • (4)未回收則開啓服務,dump heap獲取內存快照hprof文件
  • (5)解析hprof文件根據 KeyedWeakReference類型過濾找到內存泄漏對象
  • (6)計算對象到GC roots的最短路徑,併合並全部最短路徑爲一棵樹
  • (7)輸出分析結果,並根據分析結果展現到可視化頁面

  除了這些外,LeakCanary中代碼風格一樣值得學習,包括巧用ContentProvider初始化,kolint類委託進行選擇性方法實現等。

參考文檔:
[1] Android 中的引用類型初探
[2] java 源碼系列 - 帶你讀懂 Reference 和 ReferenceQueue
[3] 深刻理解 Android 之 LeakCanary 源碼解析

相關文章
相關標籤/搜索