2020.02.15 15時html
在 Android 開發工做中,內存泄露一直是讓人比較頭疼的問題。首先內存泄露並非一個 Java 異常,因此咱們並不能實時感知到它,通常只有等到內存溢出的時候,咱們纔會去排除是否發生了內存泄露問題。而每每致使拋異常的代碼並非內存泄露的兇手,而只是壓死駱駝的最後一根稻草而已,這是第一個問題。第二個問題則是,當咱們想要分析內存問題時,首先須要先 dump 內存快照,一般是以 .hprof 結尾的文件,接着再使用 MAT 等內存分析工具去檢測大內存可疑對象,分析對象到 GC Roots 節點的可達性等等。整個流程相對繁瑣,這時候咱們可能會考慮是否有自動化工具,來幫助咱們去分析那些常見的內存泄露場景呢。java
LeakCanary 就是爲了解決以上問題而誕生的。2019 年 11 月 的時候,LeakCanary2 正式版發佈,和 LeakCanary1 相比,LeakCanary2 有如下改動:node
其中,將 Heap 分析模塊做爲一個獨立的模塊,是一個很是不錯的改動。這意味着,能夠基於 Shark 來作不少有意思的事情,好比,用於線上分析或者開發一個"本身"的 LeakCanary。android
在分析源碼以前,咱們先看 LeakCanary 的總體結構,這有助於咱們對項目總體設計上有必定理解。LeakCanary2 有如下幾個模塊:c++
leakcanary-androidgit
集成入口模塊,提供 LeakCanary 安裝,公開 API 等能力github
leakcanary-android-core數組
核心模塊緩存
leakcanary-android-process架構
和 leakcanary-android 同樣,區別是會在單獨的進程進行分析
leakcanary-android-instrumentation
用於 Android Test 的模塊
leakcanary-object-watcher-android,leakcanary-object-watcher-android-androidx,leakcanary-watcher-android-support-fragments
對象實例觀察模塊,在 Activity,Fragment 等對象的生命週期中,註冊對指定對象實例的觀察,有 Activity,Fragment,Fragment View,ViewModel 等
shark-android
提供特定於 Android 平臺的分析能力。例如設備的信息,Android 版本,已知的內存泄露問題等
shark,shark-test
hprof 文件解析與分析的入口模塊,還有對應的 Test 模塊
shark-graph
分析堆中對象的關係圖模塊
shark-hprof,shark-hprof-test
解析 hprof 文件模塊,還有對應的 Test 模塊
shark-log
日誌模塊
shark-cli
shark-android 的 cli 版本
首先,咱們從集成方式入手,LeakCanary1 的依賴爲:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
}
複製代碼
接着在 Application 中調用 LeakCanary.install()
方法。而 LeakCanary2 集成則要簡單很多,只須要增長如下依賴便可:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}
複製代碼
也就是說 LeakCanary2 實現了自動調用 install()
方法,實現方式可能大部分人都能猜到,就是使用的 ContentProvider
,相關代碼位於 leakcanary-object-watcher-android 模塊中的 AppWatcherInstaller.kt
中。
AppWatcherInstaller
繼承 ContentProvider
,重寫了 onCreate()
方法,這裏利用的是,註冊在 Manifest 文件中的 ContentProvider
,會在應用啓動時,由 ActivityThread
建立並初始化。
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
InternalAppWatcher.install(application)
return true
}
複製代碼
AppWatcherInstaller
有兩個實現類,一個是 MainProcess
,當咱們使用 leakcanary-android 模塊時,會默認使用這個,表示在當前 App 進程中使用 LeakCanary。另一個類爲 LeakCanaryProcess
,當使用 leakcanary-android-process 模塊代替 leakcanary-android 模塊時,則會使用這個類,咱們能夠看下 leakcanary-android-process 的 Manifest 文件:
這裏利用的 leakcanary-android-process Manifest 優先級要高於 leakcanary-object-watcher-android
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.squareup.leakcanary">
<application>
<service android:name="leakcanary.internal.HeapAnalyzerService" android:exported="false" android:process=":leakcanary" />
<provider android:name="leakcanary.internal.AppWatcherInstaller$LeakCanaryProcess" android:authorities="${applicationId}.leakcanary-process.installer" android:process=":leakcanary" android:exported="false"/>
</application>
</manifest>
複製代碼
能夠看到,HeapAnalyzerService
和 LeakCanaryProcess
都會在運行在 :leakcanary
進程,關於 HeapAnalyzerService
的做用,咱們後面會講到。
這裏有個須要注意的是,若是使用 LeakCanaryProcess
,默認會禁用 Watcher 功能,這個也很好理解,處於不一樣進程,是沒辦法觀察到 APP 進程的對象。
internal class LeakCanaryProcess : AppWatcherInstaller() {
override fun onCreate(): Boolean {
super.onCreate()
AppWatcher.config = AppWatcher.config.copy(enabled = false)
return true
}
}
複製代碼
Watcher 功能的入口位於 InternalAppWatcher.install()
方法中,這個方法的調用時機則是咱們上面說到的 AppWatcherInstaller.onCreate()
中。
fun install(application: Application) {
ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
onAppWatcherInstalled(application)
}
複製代碼
這裏主要作了兩件事,首先是 Activity 和 Fragment 對象的註冊觀察,這裏咱們以 ActivityDestroyWatcher
爲例,Fragment 的處理也是相似的。
fun install( application: Application, objectWatcher: ObjectWatcher, configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
複製代碼
調用 registerActivityLifecycleCallbacks()
方法註冊 Activity 生命週期回調。
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
objectWatcher.watch(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
複製代碼
在每一個 Activity.onDestory
回調中,將每一個 Activity 對象加到觀察列表中。
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>()
@Synchronized fun watch( watchedObject: Any, description: String ) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
複製代碼
首先咱們要知道 KeyedWeakReference
繼承於 WeakReference
,弱引用是不會阻止 GC 回收對象的,同時咱們能夠在構造函數中傳遞一個 ReferenceQueue
,用於對象被 GC 後存放的隊列。
class KeyedWeakReference(
referent: Any,
val key: String,
val description: String,
val watchUptimeMillis: Long,
referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>
複製代碼
因此 removeWeaklyReachableObjects()
方法的做用就是將已經被 GC 的對象從 watchedObjects
集合中刪除。
當咱們調用 watch()
方法時,先清理已經被 GC 的對象,接着將須要觀察的對象,存儲爲一個 KeyedWeakReference
的弱引用對象,再存放到 watchedObjects
集合中,最後使用 checkRetainedExecutor
安排一次 moveToRetained
任務。
checkRetainedExecutor
是使用 Handler 實現,默認延遲 5s 執行任務。
val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
private val checkRetainedExecutor = Executor {
mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)
}
複製代碼
接下來,咱們再來看下 moveToRetained()
的代碼:
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
複製代碼
一樣的,先調用一次 removeWeaklyReachableObjects()
刪除已經 GC 的對象,那麼剩下的對象就能夠認爲是被保留(沒辦法 GC)的對象,會調通知事件。
在 InternalAppWatcher.install()
方法的最後,還有一個 onAppWatcherInstalled
的調用,它是一個方法對象,在 Kotlin 中一切皆對象,包括方法,它的賦值在 init 塊中:
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
}
複製代碼
這段代碼對不熟悉 Kotlin 的小夥伴來講可能有點繞,首先 internalLeakCanary
是一個方法對象,它的方法簽名轉化爲 Java 代碼爲:
void invoke(Application) {
}
複製代碼
而這個對象的值是經過反射獲取的 InternalLeakCanary.INSTANCE
這是一個單例對象。InternalLeakCanary
位於 leakcanary-android-core 模塊,這也是須要反射的緣由,這樣的處理值得商榷。
當調用 onAppWatcherInstalled()
方法時,實際會調用 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
)
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
registerResumedActivityListener(application)
addDynamicShortcut(application)
disableDumpHeapInTests()
}
複製代碼
這裏作的事情比較多,首先是 addOnObjectRetainedListener()
方法,這裏會註冊一個 OnObjectRetainedListener
事件,也就是咱們上面說到的在 moveToRetained()
方法中的回調事件。
AndroidHeapDumper
則是經過調用 Debug.dumpHprofData()
方法從虛擬機中 dump hprof 文件。
GcTrigger
經過調用 Runtime.getRuntime().gc()
方法觸發虛擬機進行 GC 操做。
HeapDumpTrigger
管理觸發 Heap Dump 的邏輯,有兩個地方會觸發 Heap Dump:
保留對象超過闕值
這個闕值默認爲 5(應用可見的狀況下),能夠經過 Config
配置:
val retainedVisibleThreshold: Int = 5
複製代碼
當 ObjectWatcher
回調 onObjectRetained()
方法時,HeapDumpTrigger.onObjectRetained()
方法會被調用:
fun onObjectRetained() {
scheduleRetainedObjectCheck(
reason = "found new object retained",
rescheduling = false
)
}
private fun scheduleRetainedObjectCheck( reason: String, rescheduling: Boolean, delayMillis: Long = 0L ) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
// 同時只會有一個任務
val scheduledIn = checkCurrentlyScheduledAt - SystemClock.uptimeMillis()
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects(reason)
}, delayMillis)
}
private fun checkRetainedObjects(reason: String) {
val config = configProvider()
if (!config.dumpHeap) {
return
}
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
// 先執行一次 GC
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
// 檢測當前保留對象數量
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
// 默認 debug 時不執行,從新安排到 20s 後
scheduleRetainedObjectCheck(
reason = "debugger is attached",
rescheduling = true,
delayMillis = WAIT_FOR_DEBUG_MILLIS
)
return
}
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
// 60s 內只會執行一次,從新安排
scheduleRetainedObjectCheck(
reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
rescheduling = true,
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
dismissRetainedCountNotification()
// 執行 dump heap
dumpHeap(retainedReferenceCount, retry = true)
}
private fun checkRetainedCount( retainedKeysCount: Int, retainedVisibleThreshold: Int ): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
if (retainedKeysCount == 0) {
// 沒有保留對象
return true
}
if (retainedKeysCount < retainedVisibleThreshold) {
// 低於闕值
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
// 當前應用可見,或者不可見時間間隔少於 5s,從新安排到 2s 後
scheduleRetainedObjectCheck(
reason = "found only $retainedKeysCount retained objects (< $retainedVisibleThreshold while app visible)",
rescheduling = true,
delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}
複製代碼
上面的代碼稍微有點長,因此在關鍵代碼處添加了註釋。在執行 heap dump 以前,須要處理幾種狀況,好比當前是否是處於調試模式,距離上一次執行有沒有超過 60s,當前應用是否處於可見狀態等等,最終執行的方法是 dumpHeap()
:
private fun dumpHeap( retainedReferenceCount: Int, retry: Boolean ) {
val heapDumpFile = heapDumper.dumpHeap()
// 由於這些對象咱們已經 dump 出來分析了,因此不必保留它們了
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
HeapAnalyzerService.runAnalysis(application, heapDumpFile)
}
// ObjectWatcher.kt
@Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
val weakRefsToRemove =
watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
weakRefsToRemove.values.forEach { it.clear() }
watchedObjects.keys.removeAll(weakRefsToRemove.keys)
}
複製代碼
首先調用 HeapDumper.dumpHeap()
獲取 hprof 文件,接着調用 ObjectWatcher.clearObjectsWatchedBefore()
方法清理,最後調用 HeapAnalyzerService.runAnalysis()
進行分析。
從 ObjectWatcher
保存弱引用對象,再到 HeapDumpTrigger
觸發 heap dump,整個過程是很是清晰的。
HeapAnalyzerService
是繼承於 IntentService
,調用 runAnalysis()
方法最終會調用到 analyzeHeap()
方法:
private fun analyzeHeap( heapDumpFile: File, config: Config ): HeapAnalysis {
val heapAnalyzer = HeapAnalyzer(this)
val proguardMappingReader = try {
ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
} catch (e: IOException) {
null
}
return heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
leakingObjectFinder = config.leakingObjectFinder,
referenceMatchers = config.referenceMatchers,
computeRetainedHeapSize = config.computeRetainedHeapSize,
objectInspectors = config.objectInspectors,
metadataExtractor = config.metadataExtractor,
proguardMapping = proguardMappingReader?.readProguardMapping()
)
}
複製代碼
proguardMappingReader
是用於處理代碼混淆的,支持在測試版本打開代碼混淆開關,PROGUARD_MAPPING_FILE_NAME
表示 Mapping 文件,這個文件在 leakcanary-deobfuscation-gradle-plugin 模塊處理的,具體的能夠看 CopyObfuscationMappingFileTask
。
HeapAnalyzer.analyze()
這個方法的做用是:從 hprof 文件中搜索泄露對象,而後計算它們到 GC Roots 的最短路徑。
Hprof.open(heapDumpFile)
.use { hprof ->
val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
val helpers =
FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
helpers.analyzeGraph(
metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
)
}
複製代碼
首先,經過調用 Hprof.open()
讀取 hprof 文件:
fun open(hprofFile: File): Hprof {
val fileLength = hprofFile.length()
if (fileLength == 0L) {
throw IllegalArgumentException("Hprof file is 0 byte length")
}
val inputStream = hprofFile.inputStream()
val channel = inputStream.channel
val source = Okio.buffer(Okio.source(inputStream))
val endOfVersionString = source.indexOf(0)
val versionName = source.readUtf8(endOfVersionString)
val hprofVersion = supportedVersions[versionName]
// Skip the 0 at the end of the version string.
source.skip(1)
val identifierByteSize = source.readInt()
// heap dump timestamp
val heapDumpTimestamp = source.readLong()
val byteReadCount = endOfVersionString + 1 + 4 + 8
val reader = HprofReader(source, identifierByteSize, byteReadCount)
return Hprof(
channel, source, reader, heapDumpTimestamp, hprofVersion, fileLength
)
}
複製代碼
關於 hprof 文件
hprof 是由 JVM TI Agent HPROF 生成的一種二進制文件,關於 hprof 文件格式能夠看這裏。
hprof 文件能夠分爲如下幾個部分:
header
header 中有如下三個部分組成:
文件格式名和版本號
JDK 1.6 的值爲 "JAVA PROFILE 1.0.2",在此以前還有 "JAVA PROFILE 1.0" 和 「JAVA PROFILE 1.0.1」,若是是 Android 平臺的話,這個值爲 "JAVA PROFILE 1.0.3",這也是爲何 MAT 不支持直接解析 Android 平臺生成的 hprof 文件了。
identifiers
4 字節,表示 ID 的大小,它的值可能爲 4 或者 8,表示一個 ID 須要用 4 字節或者 8 字節來表示。
時間戳
高位 4 字節 + 低位 4 字節
records
record 表示文件中記錄的信息,每一個 record 都由如下 4 個部分組成:
tag
1 字節,表示 record 的類型,支持的值能夠看文檔。
time
4 字節,表示 record 的時間戳。
length
4 字節,表示 body 的字節長度。
body
表示 record 中存儲的數據。
JVM TI(JVM tool interface)表示虛擬機工具接口,用於提供查詢和控制虛擬機中運行的程序。
Agent 表示代理程序,用於調用 JVM TI 的,會運行在 JVM 的進程中,通常經過
java -agentlib
或java -agentpath
啓動。
在瞭解完 hprof 文件的格式後,咱們再來看 LeakCanary 的解析 hprof 文件的代碼,一樣的,先依次讀取 versionName
、identifierByteSize
、heapDumpTimestamp
後,再建立一個 HprofReader
用於讀取 records。HprofReader
的工做方式也是相似的,根據 tag 的值去讀取不一樣的 record,這裏咱們以 "STRING IN UTF8" 爲例:
when (tag) {
STRING_IN_UTF8 -> {
if (readStringRecord) {
val recordPosition = position
val id = readId()
val stringLength = length - identifierByteSize
val string = readUtf8(stringLength)
val record = StringRecord(id, string)
listener.onHprofRecord(recordPosition, record)
} else {
skip(length)
}
}
}
複製代碼
HeapGraph
用於表示 heap 中的對象關係圖,經過調用 HprofHeapGraph.indexHprof()
生成:
fun indexHprof( hprof: Hprof, proguardMapping: ProguardMapping? = null, indexedGcRootTypes: Set<KClass<out GcRoot>> = setOf( JniGlobal::class, JavaFrame::class, JniLocal::class, MonitorUsed::class, NativeStack::class, StickyClass::class, ThreadBlock::class, ThreadObject::class, JniMonitor::class )
): HeapGraph {
val index = HprofInMemoryIndex.createReadingHprof(hprof, proguardMapping, indexedGcRootTypes)
return HprofHeapGraph(hprof, index)
}
複製代碼
indexedGcRootTypes
表示咱們要收集的 GC Roots 節點,能夠做爲 GC Roots 節點的有如下對象:
// Traditional.
HPROF_ROOT_UNKNOWN = 0xFF,
// native 中的全局變量
HPROF_ROOT_JNI_GLOBAL = 0x01,
// native 中的局部變量
HPROF_ROOT_JNI_LOCAL = 0x02,
// java 中的局部變量
HPROF_ROOT_JAVA_FRAME = 0x03,
// native 中的入參和出參
HPROF_ROOT_NATIVE_STACK = 0x04,
// 系統類
HPROF_ROOT_STICKY_CLASS = 0x05,
// 活動線程引用的對象
HPROF_ROOT_THREAD_BLOCK = 0x06,
// 調用 wait() 或者 notify(),或者 synchronized 的對象
HPROF_ROOT_MONITOR_USED = 0x07,
// 活動線程
HPROF_ROOT_THREAD_OBJECT = 0x08,
// Android.
// 調用 String.intern() 的對象
HPROF_ROOT_INTERNED_STRING = 0x89,
// 等待 finalizer 調用的對象
HPROF_ROOT_FINALIZING = 0x8a, // Obsolete.
// 用於鏈接 debugger 的對象
HPROF_ROOT_DEBUGGER = 0x8b,
// 未知
HPROF_ROOT_REFERENCE_CLEANUP = 0x8c, // Obsolete.
// 未知
HPROF_ROOT_VM_INTERNAL = 0x8d,
// 未知
HPROF_ROOT_JNI_MONITOR = 0x8e,
// 不可達,但不是 GC Root
HPROF_UNREACHABLE = 0x90, // Obsolete.
複製代碼
上面的是 JVM 定義的,下面的是 Android 平臺特有的,具體能夠看 hprof.cc。
雖然存在很多 GC Roots 節點,但 LeakCanary 只選取了部分:
HPROF_ROOT_JNI_GLOBAL
native 中的全局變量
HPROF_ROOT_JAVA_FRAME
java 中的局部變量
HPROF_ROOT_JNI_LOCAL
native 中的局部變量
HPROF_ROOT_MONITOR_USED
調用 wait() 或者 notify(),或者 synchronized 的對象
HPROF_ROOT_NATIVE_STACK
native 中的入參和出參
HPROF_ROOT_STICKY_CLASS
系統類
HPROF_ROOT_THREAD_BLOCK
活動線程引用的對象
HPROF_ROOT_THREAD_OBJECT
活動線程
HPROF_ROOT_JNI_MONITOR
未知,多是 native 中的同步對象
接着會從 hprof 文件中讀取 records,讀取原理能夠參考 hprof 文件格式。這裏有個小細節,LeakCanary 只會讀取如下幾種類型的 record:
STRING IN UTF8
0x01,UTF8 格式的字符串
LOAD CLASS
0x02,虛擬機中加載的類
HEAP DUMP 中的 CLASS DUMP
0x0C 和 0x20,dump 出來內存中的類實例
hprof 1.0.2 版本會用 HEAP DUMP SEGMENT 0x1C 做用是同樣的
HEAP DUMP 中的 INSTANCE DUMP
0x0C 和 0x21,dump 出來內存中的對象實例
HEAP DUMP 中的 OBJECT ARRAY DUMP
0x0C 和 0x22,dump 出來內存中的對象數組實例
HEAP DUMP 中的 PRIMITIVE ARRAY DUMP
0x0C 和 0x23,dump 出來內存中的原始類型數組實例
HEAP 中 GC Roots
這裏包括了上面定義的全部 GC Roots 對象實例
在生成 heap graph 後,咱們就能夠根據它,來獲取泄露對象的 objectIds:
// FindLeakInput.analyzeGraph()
val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
複製代碼
LeakingObjectFinder
用於查詢泄露對象,它的實現有兩個:KeyedWeakReferenceFinder
和 FilteringLeakingObjectFinder
,默認爲 KeyedWeakReferenceFinder
,即經過 KeyedWeakReference
引用的對象,關於 KeyedWeakReference
的做用咱們在 AppWatcher 那裏有說到。
internal fun findKeyedWeakReferences(graph: HeapGraph): List<KeyedWeakReferenceMirror> {
return graph.context.getOrPut(KEYED_WEAK_REFERENCE.name) {
val addedToContext: List<KeyedWeakReferenceMirror> = graph.instances
.filter { instance ->
val className = instance.instanceClassName
className == "leakcanary.KeyedWeakReference" || className == "com.squareup.leakcanary.KeyedWeakReference"
}
.map {
KeyedWeakReferenceMirror.fromInstance(
it, heapDumpUptimeMillis
)
}
.filter { it.hasReferent }
.toList()
graph.context[KEYED_WEAK_REFERENCE.name] = addedToContext
addedToContext
}
}
複製代碼
KeyedWeakReferenceFinder
經過過濾 heap dump 中的全部 KeyedWeakReference
實例,來獲取泄露對象實例。
而 FilteringLeakingObjectFinder
則是用於咱們自定義的泄露對象判斷邏輯:
override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> {
return graph.objects
.filter { heapObject ->
filters.any { filter ->
filter.isLeakingObject(heapObject)
}
}
.map { it.objectId }
.toSet()
}
複製代碼
LeakCanary 定義了兩個泄露類型:ApplicationLeak
和 LibraryLeak
:
ApplicationLeak
表示應用自己致使內存泄露
LibraryLeak
表示依賴庫致使的內存泄露,例如 Android Framework 等
以上兩種泄露都是經過調用 FindLeakInput.findLeaks()
方法來獲取的:
private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
val pathFinder = PathFinder(graph, listener, referenceMatchers)
val pathFindingResults =
pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
return buildLeakTraces(pathFindingResults)
}
複製代碼
這是經過 PathFinder.findPathsFromGcRoots()
方法實現的:
fun findPathsFromGcRoots( leakingObjectIds: Set<Long>, computeRetainedHeapSize: Boolean ): PathFindingResults {
val sizeOfObjectInstances = determineSizeOfObjectInstances(graph)
val state = State(leakingObjectIds, sizeOfObjectInstances, computeRetainedHeapSize)
return state.findPathsFromGcRoots()
}
private fun State.findPathsFromGcRoots(): PathFindingResults {
enqueueGcRoots()
// 省略
return PathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)
}
複製代碼
State.findPathsFromGcRoots()
的代碼有點長,咱們一點點分析。
首先是 enqueueGcRoots()
方法,它的做用是將全部 GC Roots 節點放入到隊列中:
private fun State.enqueueGcRoots() {
// 將 GC Roots 進行排序
// 排序是爲了確保 ThreadObject 在 JavaFrames 以前被訪問,這樣能夠經過 ThreadObject.threadsBySerialNumber 獲取它的線程信息
val gcRoots = sortedGcRoots()
// 存儲線程名稱
val threadNames = mutableMapOf<HeapInstance, String>()
// 存儲線程的 SerialNumber,能夠經過 SerialNumber 訪問對應的線程信息
val threadsBySerialNumber = mutableMapOf<Int, Pair<HeapInstance, ThreadObject>>()
gcRoots.forEach { (objectRecord, gcRoot) ->
if (computeRetainedHeapSize) {
// 計算泄露對象而保留的內存大小
undominateWithSkips(gcRoot.id)
}
when (gcRoot) {
is ThreadObject -> {
// 活動的 Thread 實例
// 緩存 threadsBySerialNumber
threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot
// 入列 NormalRootNode
enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
is JavaFrame -> {
// Java 局部變量
val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
if (threadPair == null) {
// Could not find the thread that this java frame is for.
enqueue(NormalRootNode(gcRoot.id, gcRoot))
} else {
val (threadInstance, threadRoot) = threadPair
val threadName = threadNames[threadInstance] ?: {
val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
threadNames[threadInstance] = name
name
}()
// RefreshceMatchers 用於匹配已知的引用節點
// IgnoredReferenceMatcher 表示忽略這個引用節點
// LibraryLeakReferenceMatcher 表示這是庫內存泄露對象
val referenceMatcher = threadNameReferenceMatchers[threadName]
if (referenceMatcher !is IgnoredReferenceMatcher) {
val rootNode = NormalRootNode(threadRoot.id, gcRoot)
val refFromParentType = LOCAL
val refFromParentName = ""
val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
LibraryLeakChildNode(
objectId = gcRoot.id,
parent = rootNode,
refFromParentType = refFromParentType,
refFromParentName = refFromParentName,
matcher = referenceMatcher
)
} else {
NormalNode(
objectId = gcRoot.id,
parent = rootNode,
refFromParentType = refFromParentType,
refFromParentName = refFromParentName
)
}
// 入列 LibraryLeakChildNode 或 NormalNode
enqueue(childNode)
}
}
}
is JniGlobal -> {
// Native 全局變量
// 是否匹配已知引用節點
val referenceMatcher = when (objectRecord) {
is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]
is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]
is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
}
if (referenceMatcher !is IgnoredReferenceMatcher) {
if (referenceMatcher is LibraryLeakReferenceMatcher) {
// 入列 LibraryLeakRootNode
enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))
} else {
// 入列 NormalRootNode
enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
}
}
// 其餘 GC Roots,入列 NormalRootNode
else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
}
}
複製代碼
在將 GC Roots 節點入列的過程,有兩個地方值得注意:
ReferenceMatcher
ReferenceMatcher
用於匹配引用節點,判斷是否要忽略它。LeakCanary 支持 4 種類型的匹配:
類實例字段
緩存在 fieldNameByClassName
裏,例如,android.os.Message
中的 obj
字段
類靜態字段
緩存在 staticFieldNameByClassName
裏,例如,android.app.ActivityManager
的 mContext
字段
指定線程
緩存在 threadNames
裏,例如,FinalizerWatchdogDaemon
線程
Native 全局變量
緩存在 jniGlobals
裏,例如,android.widget.Toast\$TN
類
內置的引用節點匹配爲 AndroidReferenceMatchers.appDefaults
。
VisitQueue
PathFinder
中有兩個隊列,一個優先級更高的 toVisitQueue
,另一個是 toVisitLastQueue
,同時提供 toVisitSet
和 toVisitLastSet
用於提供常數級查詢。
隊列中的節點分爲兩種:
RootNode
根節點,它有兩個實現類:
LibraryLeakRootNode
依賴庫的泄露根節點
NormalRootNode
普通的根節點
ChildNode
子節點,能夠經過 parent
字段訪問父節點。它有兩個實現類:
LibraryLeakChildNode
依賴庫的泄露子節點
NormalNode
普通的字節點
如下 3 種狀況會將節點放入到 toVisitLastQueue
中:
由於這 3 種致使的內存泄露狀況比較少,因此下降它們的訪問優先級。
val visitLast =
node is LibraryLeakNode ||
// We deprioritize thread objects because on Lollipop the thread local values are stored
// as a field.
(node is RootNode && node.gcRoot is ThreadObject) ||
(node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)
複製代碼
在將全部的 GC Roots 節點入列後,使用廣度優先遍歷全部的節點,當訪問節點是泄露節點,則添加到 shortestPathsToLeakingObjects
中:
val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
visitingQueue@ while (queuesNotEmpty) {
val node = poll()
if (checkSeen(node)) {
throw IllegalStateException(
"Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"
)
}
if (node.objectId in leakingObjectIds) {
shortestPathsToLeakingObjects.add(node)
// Found all refs, stop searching (unless computing retained size)
if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {
if (computeRetainedHeapSize) {
listener.onAnalysisProgress(FINDING_DOMINATORS)
} else {
break@visitingQueue
}
}
}
when (val heapObject = graph.findObjectById(node.objectId)) {
is HeapClass -> visitClassRecord(heapObject, node)
is HeapInstance -> visitInstance(heapObject, node)
is HeapObjectArray -> visitObjectArray(heapObject, node)
}
}
複製代碼
在遍歷子節點時,有 3 種狀況須要考慮:
HeapClass
當節點表示 HeapClass,咱們將它的靜態變量入列:
val node = when (val referenceMatcher = ignoredStaticFields[fieldName]) {
null -> NormalNode(
objectId = objectId,
parent = parent,
refFromParentType = STATIC_FIELD,
refFromParentName = fieldName
)
is LibraryLeakReferenceMatcher -> LibraryLeakChildNode(
objectId = objectId,
parent = parent,
refFromParentType = STATIC_FIELD,
refFromParentName = fieldName,
matcher = referenceMatcher
)
// 忽略 IgnoredReferenceMatcher
is IgnoredReferenceMatcher -> null
}
if (node != null) {
enqueue(node)
}
複製代碼
HeapInstance
當節點表示 HeapInstance,咱們將它的實例變量入列:
val fieldNamesAndValues = instance.readFields()
.filter { it.value.isNonNullReference }
.toMutableList()
fieldNamesAndValues.sortBy { it.name }
fieldNamesAndValues.forEach { field ->
val objectId = field.value.asObjectId!!
if (computeRetainedHeapSize) {
updateDominatorWithSkips(parent.objectId, objectId)
}
val node = when (val referenceMatcher = fieldReferenceMatchers[field.name]) {
null -> NormalNode(
objectId = objectId,
parent = parent,
refFromParentType = INSTANCE_FIELD,
refFromParentName = field.name
)
is LibraryLeakReferenceMatcher ->
LibraryLeakChildNode(
objectId = objectId,
parent = parent,
refFromParentType = INSTANCE_FIELD,
refFromParentName = field.name,
matcher = referenceMatcher
)
// 忽略 IgnoredReferenceMatcher
is IgnoredReferenceMatcher -> null
}
if (node != null) {
enqueue(node)
}
}
複製代碼
HeapObjectArray
當節點表示 HeapObjectArray,咱們將它的非空元素入列:
val nonNullElementIds = record.elementIds.filter { objectId ->
objectId != ValueHolder.NULL_REFERENCE && graph.objectExists(objectId)
}
nonNullElementIds.forEachIndexed { index, elementId ->
if (computeRetainedHeapSize) {
updateDominatorWithSkips(parent.objectId, elementId)
}
val name = index.toString()
enqueue(
NormalNode(
objectId = elementId,
parent = parent,
refFromParentType = ARRAY_ENTRY,
refFromParentName = name
)
)
}
複製代碼
這裏不須要考慮 HeapPrimitiveArray
的狀況,由於原始類型不能致使內存泄露。
至此,咱們經過調用 findPathsFromGcRoots()
方法將全部泄露對象的引用節點都查詢出來了。
在經過 findPathsFromGcRoots()
獲取的節點中,一個泄露對象可能會有多個引用路徑,因此咱們還須要作一次遍歷,找到每一個泄露對象的最短路徑(致使泄露的可能性最大)。
private fun deduplicateShortestPaths(inputPathResults: List<ReferencePathNode>): List<ReferencePathNode> {
val rootTrieNode = ParentNode(0)
for (pathNode in inputPathResults) {
// Go through the linked list of nodes and build the reverse list of instances from
// root to leaking.
val path = mutableListOf<Long>()
var leakNode: ReferencePathNode = pathNode
while (leakNode is ChildNode) {
// 從父節點 -> 子節點
path.add(0, leakNode.objectId)
leakNode = leakNode.parent
}
path.add(0, leakNode.objectId)
// 這裏的做用是構建樹
updateTrie(pathNode, path, 0, rootTrieNode)
}
val outputPathResults = mutableListOf<ReferencePathNode>()
findResultsInTrie(rootTrieNode, outputPathResults)
return outputPathResults
}
private fun updateTrie( pathNode: ReferencePathNode, path: List<Long>, pathIndex: Int, parentNode: ParentNode ) {
val objectId = path[pathIndex]
if (pathIndex == path.lastIndex) {
// 當前已是葉子節點
// 替換已存在的節點,當前路徑更短
parentNode.children[objectId] = LeafNode(objectId, pathNode)
} else {
val childNode = parentNode.children[objectId] ?: {
val newChildNode = ParentNode(objectId)
parentNode.children[objectId] = newChildNode
newChildNode
}()
if (childNode is ParentNode) {
// 遞歸更新
updateTrie(pathNode, path, pathIndex + 1, childNode)
}
}
}
複製代碼
經過遍歷泄露對象節點的父節點,構建出一棵樹,多個相同泄露對象節點的不一樣路徑,最終獲取最短路徑的樹。多條最短路徑(不一樣泄露對象)最終合併成一棵樹。
從上面生成泄露對象路徑 ReferencePathNode
到最終的 LeakTrace
,這裏只是又作了一層包裝,好比經過 HeapGraph.findObjectById()
將 objectId
轉成對應的 HeapObject
:
var node: ReferencePathNode = retainedObjectNode
while (node is ChildNode) {
shortestChildPath.add(0, node)
pathHeapObjects.add(0, graph.findObjectById(node.objectId))
node = node.parent
}
val rootNode = node as RootNode
pathHeapObjects.add(0, graph.findObjectById(rootNode.objectId))
複製代碼
有兩個地方須要注意下:
ObjectInspector
經過調用 ObjectInspector.inspect()
,能夠對每一個 ObjectReporter
添加一些說明。例如,判斷 Activity
對象是否泄露:
override fun inspect( reporter: ObjectReporter ) {
reporter.whenInstanceOf("android.app.Activity") { instance ->
val field = instance["android.app.Activity", "mDestroyed"]
if (field != null) {
if (field.value.asBoolean!!) {
leakingReasons += field describedWithValue "true"
} else {
notLeakingReasons += field describedWithValue "false"
}
}
}
}
複製代碼
咱們也能夠經過 Config.objectInspectors
添加自定義的 ObjectInspector
。
LeakingStatus
經過調用 HeapAnalyzer.computeLeakStatuses()
來計算路徑上每一個節點的泄露狀態:
private fun computeLeakStatuses(leakReporters: List<ObjectReporter>): List<Pair<LeakingStatus, String>> {
val lastElementIndex = leakReporters.size - 1
var lastNotLeakingElementIndex = -1
var firstLeakingElementIndex = lastElementIndex
val leakStatuses = ArrayList<Pair<LeakingStatus, String>>()
for ((index, reporter) in leakReporters.withIndex()) {
// 經過判斷是否存在 leakingReasons 來判斷是否爲泄露節點
val resolvedStatusPair =
resolveStatus(reporter, leakingWins = index == lastElementIndex).let { statusPair ->
if (index == lastElementIndex) {
// 葉子節點確定爲泄露狀態
when (statusPair.first) {
LEAKING -> statusPair
UNKNOWN -> LEAKING to "This is the leaking object"
NOT_LEAKING -> LEAKING to "This is the leaking object. Conflicts with ${statusPair.second}"
}
} else statusPair
}
leakStatuses.add(resolvedStatusPair)
val (leakStatus, _) = resolvedStatusPair
// firstLeakingElementIndex 第一個泄露節點的下標
// lastNotLeakingElementIndex 最後一個非泄露節點的下標
if (leakStatus == NOT_LEAKING) {
lastNotLeakingElementIndex = index
// Reset firstLeakingElementIndex so that we never have
// firstLeakingElementIndex < lastNotLeakingElementIndex
firstLeakingElementIndex = lastElementIndex
} else if (leakStatus == LEAKING && firstLeakingElementIndex == lastElementIndex) {
firstLeakingElementIndex = index
}
}
val simpleClassNames = leakReporters.map { reporter ->
recordClassName(reporter.heapObject).lastSegment('.')
}
// lastNotLeakingElementIndex 以前節點不會是泄露狀態
for (i in 0 until lastNotLeakingElementIndex) {
val (leakStatus, leakStatusReason) = leakStatuses[i]
val nextNotLeakingIndex = generateSequence(i + 1) { index ->
if (index < lastNotLeakingElementIndex) index + 1 else null
}.first { index ->
leakStatuses[index].first == NOT_LEAKING
}
// Element is forced to NOT_LEAKING
val nextNotLeakingName = simpleClassNames[nextNotLeakingIndex]
leakStatuses[i] = when (leakStatus) {
UNKNOWN -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking"
NOT_LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking and $leakStatusReason"
LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking. Conflicts with $leakStatusReason"
}
}
// firstLeakingElementIndex 以後的節點爲泄露狀態
if (firstLeakingElementIndex < lastElementIndex - 1) {
// We already know the status of firstLeakingElementIndex and lastElementIndex
for (i in lastElementIndex - 1 downTo firstLeakingElementIndex + 1) {
val (leakStatus, leakStatusReason) = leakStatuses[i]
val previousLeakingIndex = generateSequence(i - 1) { index ->
if (index > firstLeakingElementIndex) index - 1 else null
}.first { index ->
leakStatuses[index].first == LEAKING
}
// Element is forced to LEAKING
val previousLeakingName = simpleClassNames[previousLeakingIndex]
leakStatuses[i] = when (leakStatus) {
UNKNOWN -> LEAKING to "$previousLeakingName↑ is leaking"
LEAKING -> LEAKING to "$previousLeakingName↑ is leaking and $leakStatusReason"
NOT_LEAKING -> throw IllegalStateException("Should never happen")
}
}
}
return leakStatuses
}
複製代碼
至此,咱們已經把 HeapAnalyzerService.analyzeHeap()
方法分析完了,下面咱們用時序圖把這個調用關係再加深下印象:
在默認實現的 DefaultOnHeapAnalyzedListener
中,當前 hprof 文件分析成功後,會回調 onHeapAnalyzed()
方法:
override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
// 入庫
val id = LeaksDbHelper(application).writableDatabase.use { db ->
HeapAnalysisTable.insert(db, heapAnalysis)
}
val (contentTitle, screenToShow) = when (heapAnalysis) {
is HeapAnalysisFailure -> application.getString(
R.string.leak_canary_analysis_failed
) to HeapAnalysisFailureScreen(id)
is HeapAnalysisSuccess -> {
val retainedObjectCount = heapAnalysis.allLeaks.sumBy { it.leakTraces.size }
val leakTypeCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
application.getString(
R.string.leak_canary_analysis_success_notification, retainedObjectCount, leakTypeCount
) to HeapDumpScreen(id)
}
}
if (InternalLeakCanary.formFactor == TV) {
showToast(heapAnalysis)
printIntentInfo()
} else {
// 顯示通知欄消息
showNotification(screenToShow, contentTitle)
}
}
複製代碼
當點擊通知欄消息後,再跳轉到 LeakActivity
:
val pendingIntent = LeakActivity.createPendingIntent(
application, arrayListOf(HeapDumpsScreen(), screenToShow)
)
複製代碼
從源碼把 LeakCanary 的核心流程分析下來,能夠看到整個項目中,不論是模塊的劃分,代碼的風格都是很是清晰,特別是用了 kotlin 重寫後,具有了不少 Java 沒有的語法糖,讓代碼的篇幅也很是精簡。總的來講,這個是一個很是不錯的學習項目。