原理概述
關于LeakCanary的原理,官網上已經給出了詳細的解釋。翻譯過來就是:1.LeakCanary使用ObjectWatcher來監控Android的生命周期。當Activity和Fragment被destroy以后,這些引用被傳給ObjectWatcher以WeakReference的形式引用著。如果gc完5秒鐘以后這些引用還沒有被清除掉,那就是內存泄露了。
2.當被泄露掉的對象達到一個閾值,LeakCanary就會把JAVA的堆棧信息dump到.hprof文件中。
3.LeakCanary用Shark庫來解析.hprof文件,找到無法被清理的引用的引用棧,然后再根據對Android系統的知識來判定是哪個實例導致的泄露。
4.通過泄露信息,LeakCanary會將一條完整的引用鏈縮減到一個小的引用鏈,其余的因為這個小的引用鏈導致的泄露鏈都會被聚合在一起。
通過官網的介紹,我們很容易就抓住了學習LeakCanary這個庫的重點:
- LeakCanary是如何使用ObjectWatcher 監控生命周期的?
- LeakCanary如何dump和分析.hprof文件的?
看官方原理總是感覺不過癮,下面我們從代碼層面上來分析。本文基于LeakCanary 2.0 beta版。
基本使用
LeakCanary的使用相當的簡單。只需要在module的build.gradle添加一行依賴,代碼侵入少。
就這樣,應用非常簡單就接入了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-android的AndroidManifest.xml中有一個ContentProvider。
然后我們查看AppWatcherInstaller的代碼,發現內部是使用InternalAppWatcher進行的install。
可以看到這里主要把Activity和Fragment區分了開來,然后分別進行注冊。Activity的生命周期監聽是借助于Application.ActivityLifecycleCallbacks。
而Fragment的生命周期監聽是借助了Activity的ActivityLifecycleCallbacks生命周期回調,當Activity創建的時候去調用FragmentManager.registerFragmentLifecycleCallbacks方法注冊Fragment的生命周期監聽。
最終,Activity和Fragment都將自己的引用傳入了ObjectWatcher.watch()進行監控。從這里開始進入到LeakCanary的引用監測邏輯。
題外話:LeakCanary 2.0版本和1.0版本相比,增加了Fragment的生命周期監聽,每個類的職責也更加清晰。但是我個人覺得使用 (Activty)->Unit 這種lambda表達式作為類的寫法不是很優雅,倒不如面向接口編程。完全可以設計成ActivityWatcher和FragmentWatcher都繼承自某個接口,這樣也方便后續擴展。
2. 引用監控
2.1 引用和GC
- 引用
首先我們先介紹一點準備知識。大家都知道,java中存在四種引用:
- 強引用:垃圾回收器絕不會回收它,當內存空間不足,Java虛擬機寧愿拋出OOM
- 軟引用:只有在內存不足的時候JVM才會回收僅有軟引用指向的對象所占的空間
- 弱引用:當JVM進行垃圾回收時,無論內存是否充足,都會回收僅被弱引用關聯的對象。
- 虛引用:和沒有任何引用一樣,在任何時候都可能被垃圾回收。
一個對象在被gc的時候,如果發現還有軟引用(或弱引用,或虛引用)指向它,就會在回收對象之前,把這個引用加入到與之關聯的引用隊列(ReferenceQueue)中去。如果一個軟引用(或弱引用,或虛引用)對象本身在引用隊列中,就說明該引用對象所指向的對象被回收了。
當軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那么這個引用對象本身就沒有價值了,如果程序中存在大量的這類對象(注意,我們創建的軟引用、弱引用、虛引用對象本身是個強引用,不會自動被gc回收),就會浪費內存。因此我們這就可以手動回收位于引用隊列中的引用對象本身。
比如我們經常看到這種用法
還有也有這樣一種用法
這樣就可以把對象和ReferenceQueue關聯起來,進行對象是否gc的判斷了。另外我們從弱引用的特征中看到,弱引用是不會影響到這個對象是否被gc的,很適合用來監控對象的gc情況。
2.GC
java中有兩種手動調用GC的方式。
2.2 監控
我們在第一節中提到,Activity和Fragment都依賴于響應的LifecycleCallback來回調銷毀信息,然后調用了ObjectWatcher.watch添加了銷毀后的監控。接下來我們看ObjectWatcher.watch做了什么操作。
這里我們看到,有一個存儲著KeyedWeakReference的ReferenceQueue對象。在每次增加watch object的時候,都會去把已經處于ReferenceQueue中的對象給從監控對象的map即watchObjects中清理掉,因為這些對象都已經被回收了。然后再去生成一個KeyedWeakReference,這個對象就是一個持有了key和監測開始時間的WeakReference對象。最后再去調用moveToRetained,相當于記錄和回調給監控方這個對象正式開始監測的時間。
那么我們現在已經拿到了需要監控的對象了,但是又是怎么去判斷這個對象已經內存泄露的呢?這就要繼續往下面看。我們主要到前面在講解InternalAppWatcher的install方法的時候,除了install了Activity和Fragment的檢測器,還調用了onAppWatcherInstalled(application)方法,看代碼發現這個方法就是InternalLeakCanary的invoke方法。
我們看到首先是初始化了heapDumper,gcTrigger,heapDumpTrigger等對象用于gc和heapDump,同時還實現了OnObjectRetainedListener,并把自己添加到了上面的onObjectRetainedListeners中,以便每個對象moveToRetained的時候,InternalLeakCanary都能獲取到onObjectRetained()的回調,回調里就只是回調了heapDumpTrigger.onObjectRetained()方法。看來都是依賴于HeapDumpTrigger這個類。
HeapDumpTrigger主要的處理邏輯都在checkRetainedObjects方法中。
那么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過于復雜,有興趣的直接查看源碼吧
3.2 對象分析
前面我們已經分析到了,HeapDumpTrigger主要是依賴于HeapAnalyzerService進行分析。那么這個HeapAnalyzerService究竟有什么玄機?讓我們繼續往下面看。可以看到HeapAnalyzerService其實是一個ForegroundService。在接收到分析的Intent后就會調用HeapAnalyzer的analyze方法。所以最終進行分析的地方就是HeapAnalyzer的analyze方法。
核心代碼如下
這段代碼中涉及到了專為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
轉載 https://mp.weixin.qq.com/s/plD0g16u0VEqVXDQJrhhpA