Android 内存泄漏检测工具 LeakCanary(Kotlin版)的实现原理
LeakCanary
是一个简单方便的内存泄漏检测框架,做 android 的同学基本都收到过 LeakCanary 检测出来的内存泄漏。目前 LeakCanary 最新版本为 2.7 版本,并且采用 kotlin 重写了一遍。最近也是对 kotlin 有了一些了解后,才敢来分析 LeakCanary 的实现原理。
-
github地址:https://github.com/square/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,大概需要几个步骤:
-
创建一个引用队列 queue
-
创建 Reference 对象,并关联引用队列 queue
-
在 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 代码细节前,先看下检测的基本流程,避免迷失在繁杂的细节中。总体流程图如下所示:
-
ObjectWatcher
创建了一个KeyedWeakReference
来监视对象. -
稍后,在后台线程中,延时检查引用是否已被清除,如果没有则触发
GC
-
如果引用一直没有被清除,它会
dumps the heap
到一个.hprof
文件中,然后将.hprof
文件存储到文件系统。 -
分析过程主要在
HeapAnalyzerService
中进行,Leakcanary2.0 以后
使用Shark
来解析hprof
文件。 -
HeapAnalyzer
获取hprof
中的所有KeyedWeakReference
,并获取objectId
-
HeapAnalyzer
计算objectId
到GC Root
的最短强引用链路径来确定是否有泄漏,然后构建导致泄漏的引用链。 -
将分析结果存储在数据库中,并显示泄漏通知。
那么检测是在什么时候开始的呢,当然是在 activity, fragment, view, service 等销毁后才去进行检测的。下面开始深入代码细节。
3.2 LeakCanary 的启动
在前面介绍使用的时候,我们只是引入了代码,都没调用,为啥 LeakCanary 就可以工作了呢?原来 LeakCanary 是使用 ContentProvider 自动初始化的,不需要再手动调用 install 方法。可以查看具体 xml 文件:
可以看到有个关键类 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 方法。
那为什么会触发呢,因为 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 的所做的事情很简单,具体如下:
-
removeWeaklyReachableObjects 先把已经回收的监控对象从 watchedObjects 中删除;
-
通过唯一表示 key,当前时间戳来为当前需要监控的对象构造一个 KeyedWeakReference,并且,所有的监控对象都是共用一个 queue;
-
把监控对象添加到 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 获取到对象,如果获取到对象了,说明该对象依然存活,这时候就会触发回调。
可以发现最终是回调到 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" ) }
-
如果
retainedObjectCount
数量大于0,则进行一次GC,
避免额外的Dump,可以尽可能的将对象回收;
-
默认情况下,如果
retainedReferenceCount<5
,不会进行Dump
,节省资源 -
如果两次
Dump
之间时间少于60s,也会直接返回,避免频繁Dump
-
调用
dumpHeap()
进行真正的Dump
操作 - 当然在真正进行 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 给了下面一个简单提醒。
很多人都把弱引用来替换强引用来解决所谓的内存泄漏,这也是修复方式最快的一种。然而 LeakCanary 并不认可这种方式。因为内存泄漏问题的本质是被引用对象存活时间超过了其生命周期,也就是他不能被正确销毁。但是你把强引用改成弱引用,会使得部分对象的存活时间短短小于原本的生命周期,而这可能会引发更多的bug,同时也会使得代码更加难以维护。
比如很多业务 api 都会让使用者注册监听某个结果的回调,但是却没有提供移除监听的方法,一旦出现内存泄漏,大家就会采用弱引用进行封装,但是由于垃圾回收的存在,可能会导致调用方无法收到结果的回调。还有就是如果业务代码写得不够好,就会出现空指针的问题。
总结
看完本文相信大家都对 LeakCanary 内存泄漏检测原理有了一定的了解。可以试着回答下面几个问题来加深对 LeakCanary 检测原理的理解。
-
LeakCanary 检测原理是什么?可以参考前面的准备知识部分。
-
LeakCanary 为啥引入依赖后就可以自己进行内存检测?
-
LeakCanary 都对哪些对象进行了监控,怎么实现的监控?
-
LeakCanary 在什么时候回触发内存泄漏检测?是定时的还是其他什么策略?
-
LeakCanary 怎么判断一个对象发生了内存泄露的?(多次GC后对象依然没有被回收)
-
LeakCanary 什么时候才会去进行 dump 操作?dump 操作是为了获取什么?
如果上述问题你都能回答出来,那么恭喜你,你已经入门 LeakCanary 了。