Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

时间:2024-01-20 20:43:15

LeakCanary 是一个简单方便的内存泄漏检测框架,做 android 的同学基本都收到过 LeakCanary 检测出来的内存泄漏。目前 LeakCanary 最新版本为 2.7 版本,并且采用 kotlin 重写了一遍。最近也是对 kotlin 有了一些了解后,才敢来分析 LeakCanary 的实现原理。

1. 准备知识

1.1 Reference

Java中的四种引用类型,我们先简单复习下

  • 强引用,对象有强引用时不能被回收

  • 软引用 SoftReference,对象只有软引用时,在内存不足时触发GC会回收该对象

  • 弱引用 WeakReference,对象只有弱引用时,下次GC就会回收该对象

  • 虚引用 PhantomReference,平常很少会用到,源码注释主要用来监听对象清理前的动作,比Java finalization更灵活,PhantomReference 需要与 ReferenceQueue 一起配合使用。

Reference 主要是负责内存的一个状态,当然它还和java虚拟机,垃圾回收器打交道。Reference 类首先把内存分为4种状态 Active,Pending,Enqueued,Inactive。

  • Active 一般来说内存一开始被分配的状态都是 Active,

  • Pending 大概是指快要被放进队列的对象,也就是马上要回收的对象,

  • Enqueued 就是对象的内存已经被回收了,我们已经把这个对象放入到一个队列中,方便以后我们查询某个对象是否被回收,

  • Inactive 就是最终的状态,不能再变为其它状态。

1.2 ReferenceQueue

引用队列,当检测到对象的可到达性更改时,垃圾回收器将已注册的引用对象添加到队列中,ReferenceQueue实现了入队(enqueue)和出队(poll),还有remove操作,内部元素head就是泛型的Reference。

1.3 简单例子

当我们想检测一个对象是否被回收了,那么我们就可以采用 Reference + ReferenceQueue,大概需要几个步骤:

  1. 创建一个引用队列 queue

  2. 创建 Reference 对象,并关联引用队列 queue

  3. 在 reference 被回收的时候,Reference 会被添加到 queue 中

创建一个引用队列
ReferenceQueue queue = new ReferenceQueue(); // 创建弱引用,此时状态为Active,并且Reference.pending为空,当前Reference.queue = 上面创建的queue,并且next=null
WeakReference reference = new WeakReference(new Object(), queue);
System.out.println(reference);
// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING
System.gc(); /* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */ /* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */
Reference reference1 = queue.remove();
System.out.println(reference1);

那这个可以用来干什么了?

可以用来检测内存泄露, github 上面 的 leekCanary 就是采用这种原理来检测的。

  • 监听 Activity 的生命周期

  • 在 onDestroy 的时候,创建相应的 Reference 和 ReferenceQueue,并启动后台进程去检测

  • 一段时间之后,从 ReferenceQueue 读取,若读取不到相应 activity 的 Reference,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 ReferenceQueue 还是读取不到相应 activity 的 Reference,可以断定是发生内存泄露了

  • 发生内存泄露之后,dump,分析 hprof 文件,找到泄露路径

那么是怎么被添加到队列里面去的呢?

Reference 类中有一个特殊的线程叫 ReferenceHandler,专门处理那些 pending 链表中的引用对象。ReferenceHandler 类是 Reference 类的一个静态内部类,继承自 Thread,所以这条线程就叫它 ReferenceHandler 线程。其中的 run 方法最终会调用 tryHandlePending 方法,具体如下:

static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 使用 'instanceof' 有时会导致OOM
// 所以在将r从链表中摘除时先进行这个操作
c = r instanceof Cleaner ? (Cleaner) r : null;
// 移除头结点,将pending指向其后一个节点
pending = r.discovered;
//从链表中移除
r.discovered = null;
} else {
// 在锁上等待可能会造成OOM,因为它会试图分配exception对象
if (waitForNotify) {
// 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过
lock.wait();
}
// 重试
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
// 重试
return true;
} catch (InterruptedException x) {
// 重试
return true;
} // 如果移除的元素是Cleaner类型,则执行其clean方法
if (c != null) {
c.clean();
return true;
} ReferenceQueue<? super Object> q = r.queue;
//对Pending状态的实例入队操作
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}

可以发现在回收的时候,会把当前的弱引用放到对应的弱引用的队列中,这和前面的例子是吻合的。具体可以阅读这篇文章 Java 学习:Reference 和 ReferenceQueue 类

2. LeakCanary使用简介

在 app 的 build.gradle 中加入依赖

dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}

LeakCanary 会自动监控 Activity、Fragment、Fragment View、RootView、Service 的泄漏。

如果需要监控其它对象的泄露,可以手动添加如下代码:

AppWatcher.objectWatcher.watch(myView, "View was detached")

3. LeakCanary检测内存泄漏的基本流程

3.1 检测流程

在介绍 LeakCanary 代码细节前,先看下检测的基本流程,避免迷失在繁杂的细节中。总体流程图如下所示:

Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

  1. ObjectWatcher 创建了一个 KeyedWeakReference 来监视对象.

  2. 稍后,在后台线程中,延时检查引用是否已被清除,如果没有则触发 GC

  3. 如果引用一直没有被清除,它会dumps the heap 到一个.hprof 文件中,然后将.hprof 文件存储到文件系统。

  4. 分析过程主要在 HeapAnalyzerService 中进行,Leakcanary2.0 以后使用 Shark 来解析hprof文件。

  5. HeapAnalyzer 获取 hprof中的所有 KeyedWeakReference,并获取objectId

  6. HeapAnalyzer计算 objectId 到 GC Root 的最短强引用链路径来确定是否有泄漏,然后构建导致泄漏的引用链。

  7. 将分析结果存储在数据库中,并显示泄漏通知。

那么检测是在什么时候开始的呢,当然是在 activity, fragment, view, service 等销毁后才去进行检测的。下面开始深入代码细节。

3.2 LeakCanary 的启动

在前面介绍使用的时候,我们只是引入了代码,都没调用,为啥  LeakCanary 就可以工作了呢?原来 LeakCanary 是使用 ContentProvider 自动初始化的,不需要再手动调用 install 方法。可以查看具体 xml 文件:

Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

可以看到有个关键类 AppWatcherInstaller,下面来看下这个类的具体内容:

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller() /**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller() override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
// 启动内存检测
AppWatcher.manualInstall(application)
return true
}
}

可以发现 AppWatcherInstaller 继承自 ContentProvider。当我们启动App时,一般启动顺序为:

Application->attachBaseContext =====>ContentProvider->onCreate =====>Application->onCreate

ContentProvider会在Application.onCreate前初始化,这样 AppWatcherInstaller 就会被调用。关于 ContentProvider 的启动流程可以看 Android ContentProvider 启动分析,这里就不展开了。在 AppWatcherInstaller 的 onCreate 方法,启动了 LeakCanary 进行内存检测。

  // AppWatcher 这是一个静态类
@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5), // 延迟5s
watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application) // 这里就是需要监控的对象
) {
checkMainThread()
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
check(retainedDelayMillis >= 0) {
"retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
}
installCause = RuntimeException("manualInstall() first called here")
this.retainedDelayMillis = retainedDelayMillis
if (application.isDebuggableBuild) {
LogcatSharkLog.install()
}
// Requires AppWatcher.objectWatcher to be set 采用反射形式进行初始化
LeakCanaryDelegate.loadLeakCanary(application) watchersToInstall.forEach {
    // 添加监控对象的回调
it.install()
}
}

manualInstall 是一个很重要的方法,并且其参数也是需要细细看的,第二个参数是延时时间5s,也就是延迟 5s 再去进行内存泄漏的检测。第三个参数就是需要监控对象的list。来看看都有哪些对象:

  fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
// activity 的监听
ActivityWatcher(application, reachabilityWatcher),
// fragment 的监听
FragmentAndViewModelWatcher(application, reachabilityWatcher),
// view 的监听
RootViewWatcher(reachabilityWatcher),
// service 的监听
ServiceWatcher(reachabilityWatcher)
)
}

可以看到这里主要对四个对象进行了监控,分别是

  • activity,通过 Application.ActivityLifecycleCallbacks 来判断 activity 是否已经销毁了;

  • fragment,fragment 的不同版本,会有不同的处理,具体可以参考 AndroidSupportFragmentDestroyWatcher, AndroidOFragmentDestroyWatcher,AndroidXFragmentDestroyWatcher 这三个类。其中还包含了对 rootView 的监控

  • rootview,通过 OnRootViewAddedListener 来进行监控,当 android.view.WindowManager.addView 调用的时候,会对其 onRootViewAdded 进行回调,从而可以获得 rootview 。

  • service,这里比较复杂,需要了解相关的源码。主要是利用反射来获取 service 相关的通知。比如获取到 mH 的 mCallback,并把自己的 callback 交给 mH,这样当 mh 收到消息就会回调 callback,然后再去调用拦截的 mCallback,这样就不会改变原有的运行轨迹。

下面来看下反射的逻辑:

internal object LeakCanaryDelegate {

  @Suppress("UNCHECKED_CAST")
// 类型是由lazy里面的代码来确定的
val loadLeakCanary by lazy {
try {
val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
leakCanaryListener.getDeclaredField("INSTANCE")
.get(null) as (Application) -> Unit // 将其转为 (参数)-> unit 类型
} catch (ignored: Throwable) {
NoLeakCanary
}
}
}

可以发现这里反射来获取  InternalLeakCanary 的实例,前面调用的方式 LeakCanaryDelegate.loadLeakCanary(application),这会触发 LeakCanaryDelegate 中的 invoke 方法。

Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

那为什么会触发呢,因为 LeakCanaryDelegate 继承了一个函数

internal object InternalLeakCanary : (Application) -> Unit

所以下面来看看 invoke 方法:

  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 }
   // 提供一个后台线程的 looper
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
   // 初始化 heapDump 触发器
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
configProvider
)
   // 添加可见性回调
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
   // 对 activity 状态的监听
registerResumedActivityListener(application)
addDynamicShortcut(application) // We post so that the log happens after Application.onCreate()
mainHandler.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()
)
}
}
}
}
}

可以发现 invoke 才是 LeakCanary 启动后初始化的核心逻辑。在这里注册了很多回调,启动了后台线程,heapdump 触发器,gc 触发器等。

到这里,关于 LeakCanary 的启动逻辑就讲完了。

3.3 如何触发检测

其实在讲到 LeakCanary 的启动逻辑的时候,就有提到有四个监控对象,当着四个对象的生命周期发生变化的时候,就会触发相应的检测流程。

下面以 ActivityWatcher 为例讲述触发检测后的逻辑。

class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher { private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
// 收到销毁的回调,就会触发下面方法的调用
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
}

其中的 reachabilityWatcher 就是下面这个:

// AppWatcher  
val objectWatcher = ObjectWatcher( // 这里需要注意的是这是一个静态变量
clock = { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isInstalled) {
"AppWatcher not installed"
}
mainHandler.postDelayed(it, retainedDelayMillis) // 延迟 5s 后执行 excute 操作,这里 it 个人是觉得指代 excute 方法
},
isEnabled = { true }
)

因此,接下去我们需要去看看  ObjectWatcher 这个类的相关逻辑了。

// ObjectWatcher
 @Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) { // 一般为 true
return
}
removeWeaklyReachableObjects() // 先将一些已经回收的监控对象删除
val key = UUID.randomUUID().toString() // 获取唯一的标识
val watchUptimeMillis = clock.uptimeMillis()
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"
} watchedObjects[key] = reference // 加入观察map 中
checkRetainedExecutor.execute {
moveToRetained(key) // 可以知道, 5s 后才会执行
}
}

expectWeaklyReachable 的所做的事情很简单,具体如下:

  1. removeWeaklyReachableObjects 先把已经回收的监控对象从 watchedObjects 中删除;

  2. 通过唯一表示 key,当前时间戳来为当前需要监控的对象构造一个 KeyedWeakReference,并且,所有的监控对象都是共用一个 queue;

  3. 把监控对象添加到 watchedObjects 中;

这里有个很关键的类 KeyedWeakReference,下面来具体看看这个类的实现:

class KeyedWeakReference(
referent: Any,
val key: String,
val description: String,
val watchUptimeMillis: Long,
referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
referent, referenceQueue
)

还记得前面讲的准备知识吗?这里就用上了,可以发现 KeyedWeakReference 继承自 WeakReference,并且新增了一些额外的参数。

这里通过 activity 为例子介绍了触发检测的逻辑,所有监控对象都是在监听到其被销毁的时候才会触发检测,一旦销毁了就会把监控对象放在 watchedObjects,等待5s后再来看是否已经被回收。

3.4 回收操作

上文提到 5s 后才会去检测对象是否已经被回收。现在已经过了 5s 了,来看看监控对象是否已经被回收了。

咱们先来看看 moveToRetained 的具体逻辑:

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

可以看到的是再次调用了  removeWeaklyReachableObjects() 方法,也就是5s后,再次对 watchedObjects 的对象进行检查是否已经被回收了。

不过有一点需要注意的事,并不是对 watchedObjects 进行遍历来判断对否回收的,而是从 queue 中取出来对象就表示该对象已经被回收,watchedObjects 中删除对应的对象即可。

此处还是以 activity 为例子,参数 key 对应的是 activity;这时候会通过 key 来判断是否可以从 watchedObjects 获取到对象,如果获取到对象了,说明该对象依然存活,这时候就会触发回调。

Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

可以发现最终是回调到 InternalLeakCanary 中来的,下面看看相关逻辑:

 // InternalLeakCanary.kt
override fun onObjectRetained() = scheduleRetainedObjectCheck() fun scheduleRetainedObjectCheck() {
if (this::heapDumpTrigger.isInitialized) {
    // 这里会对依然存回的对象进行检测
heapDumpTrigger.scheduleRetainedObjectCheck()
}
}

这里调用 HeapDumpTrigger 来对存活的对象进行检测,下面看看具体的检测逻辑:

// HeapDumpTrigger.kt
fun scheduleRetainedObjectCheck(
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
// 如果从前面一路走下来,delayMillis 是为0的,也就是会立即执行
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects()
}, delayMillis)
} // 私有的方法,真正的开始检测
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap() val config = configProvider() if (iCanHasHeap is Nope) { // 也就是此时不能进行 heap dump
if (iCanHasHeap is NotifyingNope) {
// Before notifying that we can't dump heap, let's check if we still have retained object. 此时不能进行 heapdump
var retainedReferenceCount = objectWatcher.retainedObjectCount if (retainedReferenceCount > 0) {
gcTrigger.runGc() // 触发gc
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) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
   // 判断剩下的数量小于规定数量直接返回,默认是5个起步
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
   // 还未到时间,还需要再等会再进行 heapDump
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"
// 进行 heap dump
  dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}
上面的代码比较长,整理下相关知识点:
  1. 如果 retainedObjectCount 数量大于0,则进行一次 GC,避免额外的 Dump,可以尽可能的将对象回收;

  2. 默认情况下,如果 retainedReferenceCount<5,不会进行 Dump,节省资源

  3. 如果两次 Dump 之间时间少于60s,也会直接返回,避免频繁 Dump

  4. 调用 dumpHeap()进行真正的 Dump 操作

  5. 当然在真正进行 dump 前,还需要依赖 ICanHazHeap 来判断是否可以进行 heapdump,里面会做一些检查,确保 heapdump 的条件是满足的

ICanHazHeap 类很有趣,采用了 sealed,可以理解为是枚举类。

  // HeapDumpControl.kt
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)
}

简单来说就是定义了几种不同类型的情况,比如 Nope 是 不可以 的意思,Yup 是 可以不错 的意思,其他两个类似。因此只有在 Yup 下才可以进行 heap dump 。

3.5 heap dump

上文讲完了回收部分,对于实在无法被回收的,这时候就采用 heap dump 来将其现出原形。

// HeapDumpTrigger.kt
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
saveResourceIdNamesToMemory()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
when (val heapDumpResult = heapDumper.dumpHeap()) {
is NoHeapDump -> { // 没有dump
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( // 显示 dump 失败通知
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_dump_failed
)
)
}
is HeapDump -> { // dump 成功
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
HeapAnalyzerService.runAnalysis(
context = application,
heapDumpFile = heapDumpResult.file,
heapDumpDurationMillis = heapDumpResult.durationMillis,
heapDumpReason = reason
)
}
}
}

HeapDumpTrigger 如其名,就是一个 dump 触发器,这里最终是调用 AndroidHeapDumper 来进行 dump 的,最后会得到 dump 的结果。

可以看到上述主要讲结果分为两类,一个是 NoHeapDump,如果需要继续尝试的话,会延迟一段时间后继续重试。另一个结果自然就是成功了。

暂时先不看结果,这里先来看看 AndroidHeapDumper dump 过程,具体代码如下:

// AndroidHeapDumper
override fun dumpHeap(): DumpHeapResult {
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump // 获取文件名,如果为 null,就直接返回 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)
} // 通知正在 dumping val toast = waitingForToast.get() return try {
val durationMillis = measureDurationMillis { // 测量 dump 耗时
Debug.dumpHprofData(heapDumpFile.absolutePath) // 将data dump 到指定文件中
}
if (heapDumpFile.length() == 0L) { // 文件长度为0,表明没有数据
SharkLog.d { "Dumped heap file is 0 byte length" }
NoHeapDump
} else {
HeapDump(file = heapDumpFile, durationMillis = durationMillis) // 存在数据,说明 dump 成功,同时记录耗时
}
} 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)
}
}

整体来看 AndroidHeapDumper dumpheap 方法先是创建一个 dump 的file,用于保存数据,最后就是调用  Debug.dumpHprofData(heapDumpFile.absolutePath) 来进行dump,其中 heapDumpFile.absolutePath 就是前面所说的文件的绝对路径。下面可以看下该文件创建的代码,如下所示:

// LeakDirectoryProvider.kt
fun newHeapDumpFile(): File? {
cleanupOldHeapDumps() var storageDirectory = externalStorageDirectory()
// ...... 省略不重要的 val fileName = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
return File(storageDirectory, fileName)
}

最终会创建一个年月日时分秒的.hprof 文件。

Debug 是 android 系统自带的方法,最终也会调用 VMDebug 来实现,这个其实就是虚拟机的了。

// android.os.Debug.java
public static void dumpHprofData(String fileName) throws IOException {
VMDebug.dumpHprofData(fileName);
}

前文提到的两种结果,其实都是继承自 DumpHeapResult,其中 HeapDump 的数据结构如下:

internal data class HeapDump(
val file: File,
val durationMillis: Long
) : DumpHeapResult()

当 dump 成功知乎,就是对 hprof 文件的分析了。Leakcanary2.0版本开源了自己实现的 hprof 文件解析以及泄漏引用链查找的功能模块(命名为shark)。

分析hprof文件的工作主要是在 HeapAnalyzerService 类中完成的。

// HeapAnalyzerService.kt
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)
}

可以看到这里启动了一个后台service 来对数据进行解析。本文由于篇幅有限,就不再讲述后面分析的逻辑,关于 hprof 后面有时间会再进行分析。

其他

关于如何修复内存泄漏的篇章中,LeakCanary 给了下面一个简单提醒。

Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理

很多人都把弱引用来替换强引用来解决所谓的内存泄漏,这也是修复方式最快的一种。然而 LeakCanary 并不认可这种方式。因为内存泄漏问题的本质是被引用对象存活时间超过了其生命周期,也就是他不能被正确销毁。但是你把强引用改成弱引用,会使得部分对象的存活时间短短小于原本的生命周期,而这可能会引发更多的bug,同时也会使得代码更加难以维护。

比如很多业务 api 都会让使用者注册监听某个结果的回调,但是却没有提供移除监听的方法,一旦出现内存泄漏,大家就会采用弱引用进行封装,但是由于垃圾回收的存在,可能会导致调用方无法收到结果的回调。还有就是如果业务代码写得不够好,就会出现空指针的问题。

总结

看完本文相信大家都对 LeakCanary 内存泄漏检测原理有了一定的了解。可以试着回答下面几个问题来加深对 LeakCanary 检测原理的理解。

  1. LeakCanary 检测原理是什么?可以参考前面的准备知识部分。

  2. LeakCanary 为啥引入依赖后就可以自己进行内存检测?

  3. LeakCanary 都对哪些对象进行了监控,怎么实现的监控?

  4. LeakCanary 在什么时候回触发内存泄漏检测?是定时的还是其他什么策略?

  5. LeakCanary 怎么判断一个对象发生了内存泄露的?(多次GC后对象依然没有被回收)

  6. LeakCanary 什么时候才会去进行 dump 操作?dump 操作是为了获取什么?

如果上述问题你都能回答出来,那么恭喜你,你已经入门 LeakCanary 了。

参考文章