LeakCanary源码解析之——dump堆栈

前言

在前一篇文章LeakCanary源码解析之——内存泄漏监测当中从源码的角度对Leakcanary中内存泄漏监测原理进行了剖析。既然监测到了内存泄漏,那么接下来就是要把堆栈给dump出来,进行堆栈分析,最终以图形化的方式展示内存泄漏堆栈。本篇文章就从源码的角度分析一下dump堆栈的过程。

本次分析的源码基于:

1
2
3
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
}

InternalLeakCanary

在前一篇文章LeakCanary源码解析之——内存泄漏监测当中我们分析到,当发现可能有内存泄漏之后,就会通过OnObjectRetainedListener接口通知出去,那么我们看下谁实现了这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {

private lateinit var heapDumpTrigger: HeapDumpTrigger

override fun invoke(application: Application) {
_application = application

checkRunningInDebuggableBuild()

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))

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)

// We post so that the log happens after Application.onCreate()
Handler().post {
// https://github.com/square/leakcanary/issues/1981
// We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
// which blocks until loaded and that creates a StrictMode violation.
backgroundHandler.post {
SharkLog.d {
when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
is Nope -> application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
}
}
}

@Suppress("ReturnCount")
private fun addDynamicShortcut(application: Application) {
//...........省略
try {
shortcutManager.addDynamicShortcuts(listOf(shortcut))
} catch (ignored: Throwable) {
SharkLog.d(ignored) {
"Could not add dynamic shortcut. " +
"shortcutCount=$shortcutCount, " +
"maxShortcutCountPerActivity=${shortcutManager.maxShortcutCountPerActivity}"
}
}
}

override fun onObjectRetained() = scheduleRetainedObjectCheck()

fun scheduleRetainedObjectCheck() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.scheduleRetainedObjectCheck()
}
}
}

发现是一个内部类InternalLeakCanary实现了这个接口,是通过在invoke方法里面通过AppWatcher.objectWatcher.addOnObjectRetainedListener(this)方式实现的这个接口,在这个invoke方法里面还初始化了一个HeapDumpTrigger对象,当收到OnObjectRetainedListener接口的onObjectRetained回调的时候,会调用scheduleRetainedObjectCheck方法,在这个方法里面会调用heapDumpTrigger.scheduleRetainedObjectCheck方法,那么可以看出这个heapDumpTrigger肯定是用于dump堆栈的(从名字也可以很明显看出来)。

到这里大家可能会抛出一个疑问:这个InternalLeakCanary是一个internal内部类,那么它的invoke方法是谁调用的呢?

在InternalAppWatcher类的init方法里面有如下一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal object InternalAppWatcher {

private val onAppWatcherInstalled: (Application) -> Unit

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是一个单例,可以看出,在InternalAppWatcher的初始化方法里面,先通过反射获取到了InternalLeakCanary对象,然后我们可以看到InternalLeakCanary是实现了一个函数类型接口的:

1
InternalLeakCanary : (Application) -> Unit

关于什么是:函数类型,参考:https://www.kotlincn.net/docs/reference/lambdas.html#函数类型。然后把internalLeakCanary强转为(Application) -> Unit,保存为onAppWatcherInstalled变量。最后在InternalAppWatcher的install方法的最后面会执行这个函数类型实例调用:onAppWatcherInstalled(application),也就会调用InternalLeakCanary的fun invoke(application: Application) 方法。

1
2
3
4
fun install(application: Application) {
//省略
onAppWatcherInstalled(application)
}

刚刚上面也说了,当收到OnObjectRetainedListener接口的onObjectRetained回调的时候,会调用scheduleRetainedObjectCheck方法,在这个方法里面会调用heapDumpTrigger.scheduleRetainedObjectCheck方法

heapDumpTrigger.scheduleRetainedObjectCheck

接下来看下scheduleRetainedObjectCheck方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun scheduleRetainedObjectCheck(
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects()
}, delayMillis)
}

这里首先会判断当前是否正在执行schedule,如果当前正在执行schedule就返回。然后会记录checkScheduledAt,根据传入的delayMillis延时之后执行checkRetainedObjects方法,在InternalLeakCanary里面调用heapDumpTrigger.scheduleRetainedObjectCheck没有传入参数,因此默认是不延时

heapDumpTrigger.checkRetainedObjects

接下来看下checkRetainedObjects方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()

val config = configProvider()

if (iCanHasHeap is Nope) {
if (iCanHasHeap is NotifyingNope) {
// Before notifying that we can't dump heap, let's check if we still have retained object.
var retainedReferenceCount = objectWatcher.retainedObjectCount

if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}

val nopeReason = iCanHasHeap.reason()
val wouldDump = !checkRetainedCount(
retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
)

if (wouldDump) {
val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = uppercaseReason
)
}
} else {
SharkLog.d {
application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
return
}
//获取残留对象的个数
var retainedReferenceCount = objectWatcher.retainedObjectCount

if (retainedReferenceCount > 0) {
//主动执行一次gc
gcTrigger.runGc()
//再次获取残留对象的个数
retainedReferenceCount = objectWatcher.retainedObjectCount
}

//这个方法里面会有一些判断条件,判断是否需要dump,如果不需要就返回false
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
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"
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}

可以看出会先通过HeapDumpControl.iCanHasHeap()方法返回一个ICanHazHeap对象,这个对象有多种类型:

1
2
3
4
5
6
7
8
9
10
sealed class ICanHazHeap {
object Yup : ICanHazHeap()
abstract class Nope(val reason: () -> String) : ICanHazHeap()
class SilentNope(reason: () -> String) : Nope(reason)

/**
* Allows manual dumping via a notification
*/
class NotifyingNope(reason: () -> String) : Nope(reason)
}

具体HeapDumpControl.iCanHasHeap()里面的实现这里就不讲了,感兴趣的同学可以深入看下。这里我们假设返回的是Yup类型,那么就会跳过第一个if条件,继续往下走。主要分为几步:

1、先通过objectWatcher.retainedObjectCount方法拿到了被objectWatcher持有的对象的个数。如果残留的对象大于0就主动执行一次gc,然后再次获取到残留对象的个数

2、通过checkRetainedCount方法判断是否需要马上dump,如果需要就返回false

3、需要马上dump之后,会检查两次dump之间的时间间隔是否小于1分钟,如果小于一分钟就会弹出一个通知说:Last heap dump was less than a minute ago,然后过一段时间再次执行scheduleRetainedObjectCheck方法

4、如果俩次dump时间间隔已经大于等于一分钟了,就会调用dumpHeap方法

heapDumpTrigger.dumpHeap

接下来看下dumpHeap方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
saveResourceIdNamesToMemory()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
when (val heapDumpResult = heapDumper.dumpHeap()) {
is NoHeapDump -> {
if (retry) {
SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
scheduleRetainedObjectCheck(
delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
)
} else {
SharkLog.d { "Failed to dump heap, will not automatically retry" }
}
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_dump_failed
)
)
}
is HeapDump -> {
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(lastHeapDumpUptimeMillis)
HeapAnalyzerService.runAnalysis(
context = application,
heapDumpFile = heapDumpResult.file,
heapDumpDurationMillis = heapDumpResult.durationMillis,
heapDumpReason = reason
)
}
}
}

在这个方法里面会先调用heapDumper.dumpHeap()方法返回一个heapDumpResult:
1、如果返回类型为NoHeapDump就代表dump heap失败

2、如果返回类型为HeapDump就代表dump heap成功,会先调用objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)方法,用于清除所有在lastHeapDumpUptimeMillis这个时间点之前创建的KeyedWeakReference对象,因为dump堆栈已经成功了,这里就不需要再持有了。然后会调用HeapAnalyzerService.runAnalysis进行堆栈分析

这个heapDumper是一个AndroidHeapDumper对象,来看下其dumpHeap方法:

AndroidHeapDumper.dumpHeap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
override fun dumpHeap(): DumpHeapResult {
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump

val waitingForToast = FutureResult<Toast?>()
showToast(waitingForToast)

if (!waitingForToast.wait(5, SECONDS)) {
SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
return NoHeapDump
}

val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Notifications.canShowNotification) {
val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
val builder = Notification.Builder(context)
.setContentTitle(dumpingHeap)
val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
}

val toast = waitingForToast.get()

return try {
val durationMillis = measureDurationMillis {
Debug.dumpHprofData(heapDumpFile.absolutePath)
}
if (heapDumpFile.length() == 0L) {
SharkLog.d { "Dumped heap file is 0 byte length" }
NoHeapDump
} else {
HeapDump(file = heapDumpFile, durationMillis = durationMillis)
}
} catch (e: Exception) {
SharkLog.d(e) { "Could not dump heap" }
// Abort heap dump
NoHeapDump
} finally {
cancelToast(toast)
notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
}
}

可以看出主要代码是调用Debug.dumpHprofData(heapDumpFile.absolutePath)方法来dump堆栈,这个是系统的方法,调用这个方法dump堆栈的时候会造成整个界面卡住,因此你会发现每次Leakcanary开始dump堆栈的时候,整个App是没法操作的,这也是为什么前面要控制两次dump的时间间隔不能小于一分钟的原因,主要是为了防止频繁dump对开发者造成的干扰。

HeapAnalyzerService.runAnalysis

最后来看下Heap分析:HeapAnalyzerService.runAnalysis方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
companion object {
fun runAnalysis(
context: Context,
heapDumpFile: File,
heapDumpDurationMillis: Long? = null,
heapDumpReason: String = "Unknown"
) {
val intent = Intent(context, HeapAnalyzerService::class.java)
intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason)
heapDumpDurationMillis?.let {
intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis)
}
startForegroundService(context, intent)
}

private fun startForegroundService(
context: Context,
intent: Intent
) {
if (SDK_INT >= 26) {
context.startForegroundService(intent)
} else {
// Pre-O behavior.
context.startService(intent)
}
}
}

可以看出这个runAnalysis方法会启动给一个ForegroundService,这个HeapAnalyzerService是继承自ForegroundService的,而ForegroundService继承自IntentService。来看下HeapAnalyzerService的onHandleIntentInForeground方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
override fun onHandleIntentInForeground(intent: Intent?) {
if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) {
SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." }
return
}

// Since we're running in the main process we should be careful not to impact it.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File
val heapDumpReason = intent.getStringExtra(HEAPDUMP_REASON_EXTRA)
val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, -1)

val config = LeakCanary.config
val heapAnalysis = if (heapDumpFile.exists()) {
analyzeHeap(heapDumpFile, config)
} else {
missingFileFailure(heapDumpFile)
}
val fullHeapAnalysis = when (heapAnalysis) {
is HeapAnalysisSuccess -> heapAnalysis.copy(
dumpDurationMillis = heapDumpDurationMillis,
metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
)
is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
}
onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
}

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

可以看出如果heapDumpFile存在就会调用下面的analyzeHeap方法,在这个方法里面最终会new一个HeapAnalyzer对象,然后调用analyze方法进行堆栈分析。

到这里,整个dump堆栈的分析过程就结束了。