0%

内存泄漏检测 - LeakCanary浅析

LeakCanary怎么使用的?入口在哪?

https://square.github.io/leakcanary/getting_started/

在gradle配置中引入依赖后就自动启用了LeakCanary

LeakCanary 会自动检测下列对象的泄露:

  1. destroyed Activity instances
  2. destroyed Fragment instances
  3. destroyed fragment View instances
  4. cleared ViewModel instances

手动观测一个对象的泄露,通过以下方法调用:AppWatcher.objectWatcher.watch(myDetachedView, “View was detached”)

LeakCanary基本原理

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  6. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
  7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

为什么引入依赖后就自动启用LeakCanary了?

因为在AndroidManifest里注册了名为AppWatcherInstaller$LeakCanaryProcess的ContentProvider,App启动时会自动调用ContentProvider的onCreate()方法

在这里会调用AppWatcher.manualInstall(application), 这就是LeakCanary启动的入口。

ContentProvider.onCreate()调用时机是:

Application.attachBaseContext() > ContentProvider.onCreate() ->Application.onCreate()

AppWatcher.manualInstall(application)做了什么?

主要是调用了

  1. ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
  2. FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
  3. onAppWatcherInstalled(application)

ActivityDestroyWatcher.install()做的是什么?

  1. 调用了application.registerActivityLifecycleCallbacks(),监听onActivityDestroyed()
  2. 在onActivityDestroyed()中调用objectWatcher.watch(activity),监听被destroyed的activity是否发生内存泄漏

FragmentDestroyWatcher.install()做了什么?

  1. 调用了application.registerActivityLifecycleCallbacks(),监听onActivityCreated()
  2. 在onActivityCreated()中调用activity.supportFragmentManager.registerFragmentLifecycleCallbacks(),监听onFragmentCreated()、onFragmentViewDestroyed()、onFragmentDestroyed()
  3. onFragmentCreated()中调用ViewModelClearedWatcher.install(),监听被clear的ViewModel是否会发生泄漏
  4. onFragmentViewDestroyed()中调用objectWatcher.watch(fragment.view),监听fragment的view是否发生内存泄漏
  5. onFragmentDestroyed()中调用objectWatcher.watch(fragment),监听fragment是否发生内存泄漏

ViewModelClearedWatcher.install()做了什么?

通过以fragment为容器,创建一个ViewModelProvider实例,再创建一个ViewModelClearedWatcher实例,ViewModelClearedWatcher是一个ViewModel;

构造ViewModelClearedWatcher时,会获取到以fragment为容器的所有的ViewModel实例,然后在ViewModelClearedWatcher的onCleared()中objectWatcher.watch()观察fragment下的所有的ViewModel实例,检查是否有ViewModel实例发生内存泄漏

onAppWatcherInstalled()做了什么?

会调用InternalLeakCanary.invoke(application)

如何判断一个对象无法被GC机制回收?

创建一个持有要检测对象的WeakReference,然后主动触发一次GC,如果这个对象能被回收,则WeakReference.get()会为null,并且这个WeakReference实例会被放到创建WeakReference对象时给构造函数传的ReferenceQueue中。

WeakReference的特点就是,只要发生垃圾回收,WeakReference持有的对象引用就为null,并且ReferenceQueue中会存入这个WeakReference;没有发生垃圾回收,ReferenceQueue中不会存入WeakReference对象。

如果主动触发GC后,ReferenceQueue中没有监测的对象对应的WeakReference,说明该对象发生了内存泄漏。

AppWatcher.objectWatcher.watch(watchedObject: Any, description: String)做了什么?

  1. 创建了一个KeyedWeakReference对象,存入一个名为watchedObjects,map,key就是UUID.randomUUID()随机生成的,唯一标识watchedObject,value类型是KeyedWeakReference

  2. 主线程中延迟一段时间(AppWatcher.config.watchDurationMillis,默认为5秒)后,然后调用moveToRetained(key)

  3. 先通过removeWeaklyReachableObjects()清理掉watchedObjects这个map中已经被垃圾回收的对象

  4. 如果再发现map里的这个KeyedWeakReference对象还存在,就标记其为retained状态,具体会赋值KeyedWeakReference的retainedUptimeMillis为当前时间,记录下被认为是retained状态时发生的时间;也就是认为没有被垃圾回收,这里其实有两种情况,一种是垃圾回收还没有发生,一种是垃圾回收发生了但是没有回收掉。

  5. 再通知所有的OnObjectRetainedListener有对象没有被回收

KeyedWeakReference是WeakReference子类,多了4个属性

  1. key:UUID.randomUUID().toString()生成的随机字符串,唯一标识当前引用对象
  2. description:描述当前对象为什么被观测
  3. watchUptimeMillis:被watch的时间
  4. retainedUptimeMillis:当前对象被认为是保留的(retained)状态时所处的时间

在watch()和moveToRetained()的一开始都会调用removeWeaklyReachableObjects()

removeWeaklyReachableObjects()是做什么的?

如果ReferenceQueue存在了某个Reference,说明已经被垃圾回收了,不需要监测内存泄漏了,从watchedObjects中移除这个Reference对象

ReferenceQueue中的Reference对象什么时候被清理?

每次从ReferenceQueue调用poll()方法,取出元素就是去除了队首元素

哪些地方注册了OnObjectRetainedListener,都做了什么?

InternalLeakCanary实现了OnObjectRetainedListener接口,在OnObjectRetainedListener.onObjectRetained()被调用时调用了scheduleRetainedObjectCheck(),然后又会调用heapDumpTrigger.scheduleRetainedObjectCheck() -> HeapDumpTrigger.checkRetainedObjects()

延迟时间为0,立刻调用

InternalLeakCanary.invoke(application)做了什么?

主要就是一件事:

在App可见或不可见时,调用HeapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)

App可见的定义是:有Activity走了onStart()但还没有走onStop()

App不可见的定义是:所有Activity走过onStop()

在App不可见时,调用HeapDumpTrigger.scheduleRetainedObjectCheck() -> HeapDumpTrigger.checkRetainedObjects()

延迟时间为AppWatcher.config.watchDurationMillis,默认为5秒

HeapDumpTrigger.checkRetainedObjects()做了什么?

获取ObjectWatcher.watchedObjects里retained的对象的个数,如果存在retained的对象,也就说明有观察的对象还没有被回收,没有被垃圾回收其实有两种情况,一种是垃圾回收还没有发生,一种是垃圾回收发生了但是没有回收掉;这里就主动调用gcTrigger.runGc()触发一次垃圾回收。

然后正常情况会调用dumpHeap()

GcTrigger.runGc()做了什么?

代码复制了android官方的代码

https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/java/lang/ref/FinalizationTester.java

  1. 先执行Runtime.getRuntime().gc()建议虚拟机发起垃圾回收,可能会执行,可能不会被执行
  2. 然后Thread.sleep(100)等待100毫秒,让回收了对象进入创建WeakReference时传入的ReferenceQueue
  3. 调用System.runFinalization(),强制调用已经失去强引用的对象的finalize()方法

HeapDumpTrigger.scheduleRetainedObjectCheck()有哪些地方调用?

InternalLeakCanary实现了OnObjectRetainedListener接口,在OnObjectRetainedListener.onObjectRetained()被调用时调用了scheduleRetainedObjectCheck(),然后又会调用heapDumpTrigger.scheduleRetainedObjectCheck() -> HeapDumpTrigger.checkRetainedObjects()

延迟时间为0,立刻调用

在App可见或不可见时,调用HeapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)

在App不可见时,调用HeapDumpTrigger.scheduleRetainedObjectCheck() -> HeapDumpTrigger.checkRetainedObjects()

延迟时间为AppWatcher.config.watchDurationMillis,默认为5秒

HeapDumpTrigger.checkRetainedObjects()中会去调用checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold),根据LeakCanary.config.retainedVisibleThreshold的注释可知,dump heap会阻塞UI线程,为了减少对开发者的打扰,内存泄露的对象个数在一定数量之内不触发dump heap,checkRetainedObjects()就是做这个检查的。

如果内存泄露的对象个数在阈值允许的数量之内,会调用HeapDumpTrigger.scheduleRetainedObjectCheck(),延迟两秒执行,继续调用HeapDumpTrigger.checkRetainedObjects()

HeapDumpTrigger.checkRetainedObjects()中如果经过了checkRetainedCount()的校验后,决定dump heap了,但是发现距离上次dump heap的时间小于60秒,就不进行dump,延迟到距离上次dump的60秒以后再执行checkRetainedObjects()

在HeapDumpTrigger.dumpHeap()中,调用heapDumper.dumpHeap()返回NoHeapDump时,也就是dump heap失败时,会再调用scheduleRetainedObjectCheck()尝试再次dump heap,延迟时间5秒

AndroidHeapDumper.dumpHeap()做了什么?

按当前时间命名创建一个hprof文件

调用Debug.dumpHprofData()输出信息到创建的文件

LeakCanary做内存泄漏检测存在什么不足?

可见其对Activity是否泄漏的判断依赖VM会将可回收的对象加入WeakReference关联的ReferenceQueue这一特性,在Demo的测试过程中我们发现这中做法在个别系统上可能存在误报,原因大致如下:

  • VM并没有提供强制触发GC的API,通过System.gc()或Runtime.getRuntime().gc()只能“建议”系统进行GC,如果系统忽略了我们的GC请求,可回收的对象就不会被加入ReferenceQueue
  • 将可回收对象加入ReferenceQueue需要等待一段时间,LeakCanary采用延时100ms的做法加以规避,但似乎并不绝对管用
  • 监测逻辑是异步的,如果判断Activity是否可回收时某个Activity正好还被某个方法的局部变量持有,就会引起误判
  • 若反复进入泄漏的Activity,LeakCanary会重复提示该Activity已泄漏

对此我们做了以下改进:

  • 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
  • 直接通过WeakReference.get()来判断对象是否已被回收,避免因延迟导致误判
  • 若发现某个Activity无法被回收,再重复判断3次,且要求从该Activity被记录起有2个以上的Activity被创建才认为是泄漏,以防在判断时该Activity被局部变量持有导致误判
  • 对已判断为泄漏的Activity,记录其类名,避免重复提示该Activity已泄漏

参考:Matrix-Android-ResourceCanary