【带着问题学】关于LeakCanary2.0的四个问题

作者 : 开心源码 本文共8753个字,预计阅读时间需要22分钟 发布时间: 2022-05-14 共196人阅读

好文推荐
作者:RicardoMJiang
转载地址:https://juejin.cn/post/6968084138125590541/

前言

LeakCanary是一个简单方便的内存泄漏检测框架,相信很多同学都用过,使用起来非常方便,它有以下几个特点
1.不需要手动初始化
2.可自动检测内存泄漏并通过通知报警
3.不能用于线上

那我们自然可以提出以下几个问题
1.说一下LeakCanary检测内存泄漏的原理与基本流程
2.LeakCanary是如何初始化的?
3.说一下LeakCanary是如何查找内存泄露的?
4.为什么LeakCanary不能用于线上?

本文主要梳理LeakCanary内存泄漏检测的主要流程并答复以上几个问题

1. LeakCanary检测内存泄漏的原理与基本流程

1.1 内存泄漏的原理

内存泄漏的起因:不再需要的对象仍然被引用,导致对象被分配的内存无法被回收。
例如:一个Activity实例对象在调用了onDestory方法后是不再被需要的,假如存储了一个引用Activity对象的静态域,将导致Activity无法被垃圾回收器回收。
引用链来自于垃圾回收器的可达性分析算法:当一个对象到GC Roots 没有任何引用链相连时,则证实此对象是不可用的。如图:

对象object5object6object7 尽管互相有关联,但是它们到 GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。
Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

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

知道了内存泄漏的原理,我们可以推测到LeakCanary的基本流程大概是怎么的
1.在页面关闭后触发检测(不再需要的对象)
2.触发GC,而后获取依然存在的对象,这些是可能泄漏的
3.dump heap而后分析hprof文件,构建可能泄漏的对象与GCRoot间的引用链,假如存在则证实泄漏
4.存储结果并使用通知提示客户存在泄漏

总体流程图如下所示:

  • 1.ObjectWatcher 创立了一个KeyedWeakReference来监视对象.
  • 2.稍后,在后端线程中,延时检查引用能否已被清理,假如没有则触发GC
  • 3.假如引用一直没有被清理,它会dumps the heap 到一个.hprof 文件中,而后将.hprof 文件存储到文件系统。
  • 4.分析过程主要在HeapAnalyzerService中进行,Leakcanary2.0中使用Shark来解析hprof文件。
  • 5.HeapAnalyzer 获取hprof中的所有KeyedWeakReference,并获取objectId
  • 6.HeapAnalyzer计算objectIdGC Root的最短强引用链路径来确定能否有泄漏,而后构建导致泄漏的引用链。
  • 7.将分析结果存储在数据库中,并显示泄漏通知。

这里只做一个总体的详情,具体流程可以阅读下文

2. LeakCanary是如何自动安装的?

LeakCanary的使用非常方便,只要要增加依赖便可以自动初始化,这是如何实现的呢?
我们看一下源码,其实主要是通过ContentProvider实现的

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  }}  复制代码

当我们启动App时,一般启动顺序为:Application->attachBaseContext =====>ContentProvider->onCreate =====>Application->onCreate
ContentProvider会在Application.onCreate前初始化,这样就调用到了LeakCanary的初始化方法
实现了免手动初始化

2.1 跨进程初始化

注意,AppWatcherInstaller有两个子类,MainProcessLeakCanaryProcess
其中默认使用MainProcess,会在App进程初始化
有时我们考虑到LeakCanary比较耗内存,需要在独立进程初始化
使用leakcanary-android-process模块的时候,会在一个新的进程中去开启LeakCanary

2.2 LeakCanary2.0手动初始化的方法

LeakCanary在检测内存泄漏时比较耗时,同时会打断App操作,在不需要检测时的体验并不太好
所以尽管LeakCanary可以自动初始化,但我们有时其实还是需要手动初始化

LeakCanary的自动初始化可以手动关闭

 <?xml version="1.0" encoding="utf-8"?> <resources>      <bool name="leak_canary_watcher_auto_install">false</bool> </resources>复制代码

1.而后在需要初始化的时候,调用AppWatcher.manualInstall就可
2.能否开始dump与分析开头:LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)
3.桌面图标开头:重写R.bool.leak_canary_add_launcher_icon或者者调用LeakCanary.showLeakDisplayActivityLauncherIcon(false)

2.3 小结

LeakCanary利用ContentProvier进行了初始化。
ContentProvier一般会在Application.onCreate之前被加载,LeakCanary在其onCreate()方法中调用了AppWatcher.manualInstall进行初始化
这种写法尽管方便,免去了初始化的步骤,但是可能会带来启动耗时的问题,客户不能控制初始化的时机,这也是谷歌推出StartUp的起因
不过对于LeakCanary这个问题并不严重,由于它只在Debug阶段被依赖

3.LeakCanary如何检测内存泄漏?

3.1 首先我们来看下初始化时做了什么?

当我们初始化时,调用了AppWatcher.manualInstall,下面来看看这个方法,都安装了什么东西

  @JvmOverloads  fun manualInstall(    application: Application,    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)  ) {    ....    watchersToInstall.forEach {      it.install()    }  }  fun appDefaultWatchers(    application: Application,    reachabilityWatcher: ReachabilityWatcher = objectWatcher  ): List<InstallableWatcher> {    return listOf(      ActivityWatcher(application, reachabilityWatcher),      FragmentAndViewModelWatcher(application, reachabilityWatcher),      RootViewWatcher(reachabilityWatcher),      ServiceWatcher(reachabilityWatcher)    )  }

可以看出,初始化时即安装了少量Watcher,即在默认情况下,我们只会观察Activity,Fragment,RootView,Service这些对象能否泄漏
假如需要观察其余对象,需要手动增加并解决

3.2 LeakCanary如何触发检测?

如上文所述,在初始化时会安装少量Watcher,我们以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"        )      }    }  override fun install() {    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)  }  override fun uninstall() {    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)  }}

可以看到在Activity.onDestory时,就会触发检测内存泄漏

3.3 LeakCanary如何检测可能泄漏的对象?

从上面可以看出,Activity关闭后会调用到ObjectWatcher.expectWeaklyReachable

@Synchronized override fun expectWeaklyReachable(    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)    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    checkRetainedExecutor.execute {      moveToRetained(key)    }  }private fun removeWeaklyReachableObjects() {    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly    // reachable. This is before finalization or garbage collection has actually happened.    var ref: KeyedWeakReference?    do {      ref = queue.poll() as KeyedWeakReference?      if (ref != null) {        watchedObjects.remove(ref.key)      }    } while (ref != null)  }  

可以看出
1.传入的观察对象都会被存储在watchedObjects
2.会为每个watchedObject生成一个KeyedWeakReference弱引用对象并与一个queue关联,当对象被回收时,该弱引用对象将进入queue当中
3.在检测过程中,我们会调用屡次removeWeaklyReachableObjects,将已回收对象从watchedObjects中移除
4.假如watchedObjects中没有移除对象,证实它没有被回收,那么就会调用moveToRetained

3.4 LeakCanary触发堆快照,生成hprof文件

moveToRetained之后会调用到HeapDumpTrigger.checkRetainedInstances方法
checkRetainedInstances() 方法是确定泄露的最后一个方法了。
这里会确认引用能否真的泄露,假如真的泄露,则发起 heap dump,分析 dump 文件,找到引用链

private fun checkRetainedObjects() {    var retainedReferenceCount = objectWatcher.retainedObjectCount    if (retainedReferenceCount > 0) {      gcTrigger.runGc()      retainedReferenceCount = objectWatcher.retainedObjectCount    }    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return    val now = SystemClock.uptimeMillis()    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {      onRetainInstanceListener.onEvent(DumpHappenedRecently)      ....      return    }    dismissRetainedCountNotification()    val visibility = if (applicationVisible) "visible" else "not visible"    dumpHeap(      retainedReferenceCount = retainedReferenceCount,      retry = true,      reason = "$retainedReferenceCount retained objects, app is $visibility"    ) }  private fun dumpHeap(    retainedReferenceCount: Int,    retry: Boolean,    reason: String  ) {     ....     heapDumper.dumpHeap()     ....     lastDisplayedRetainedObjectCount = 0     lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()     objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)     HeapAnalyzerService.runAnalysis(       context = application,       heapDumpFile = heapDumpResult.file,       heapDumpDurationMillis = heapDumpResult.durationMillis,       heapDumpReason = reason     ) }}

1.假如retainedObjectCount数量大于0,则进行一次GC,避免额外的Dump
2.默认情况下,假如retainedReferenceCount<5,不会进行Dump,节省资源
3.假如两次Dump之间时间少于60s,也会直接返回,避免频繁Dump
4.调用heapDumper.dumpHeap()进行真正的Dump操作
5.Dump之后,要删除已经解决过了的引用
6.调用HeapAnalyzerService.runAnalysis对结果进行分析

3.5 LeakCanary如何分析hprof文件

分析hprof文件的工作主要是在HeapAnalyzerService类中完成的
关于Hprof文件的解析细节,就需要牵扯到Hprof二进制文件协议,通过阅读协议文档,hprof的二进制文件结构大概如下:

解析流程如下所示:

简要说下流程:
1.解析文件头信息,得到解析开始位置
2.根据头信息创立Hprof文件对象
3.构建内存索引
4.使用hprof对象和索引构建Graph对象
5.查找可能泄漏的对象与GCRoot间的引用链来判断能否存在泄漏(使用广度优先算法在Graph中查找)

Leakcanary2.0较之前的版本最大变化是改由kotlin实现以及开源了自己实现的hprof解析的代码,总体的思路是根据hprof文件的二进制协议将文件的内容解析成一个图的数据结构,而后广度遍历这个图找到最短路径,路径的起始就是GCRoot对象,结束就是泄漏的对象

具体分析可见:Android内存泄漏检测之LeakCanary2.0(Kotlin版)的实现原理

3.6 泄漏结果存储与通知

结果的存储与通知主要在DefaultOnHeapAnalyzedListener中完成

override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {    SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }    val db = LeaksDbHelper(application).writableDatabase    val id = HeapAnalysisTable.insert(db, heapAnalysis)    db.releaseReference()    ...    if (InternalLeakCanary.formFactor == TV) {      showToast(heapAnalysis)      printIntentInfo()    } else {      showNotification(screenToShow, contentTitle)    }  }

主要做了两件事
1.存储泄漏分析结果到数据库中
2.展现通知,提示客户去查看内存泄漏情况

4.为什么LeakCanary不能用于线上?

了解了LeakCanary判定对象泄漏后所做的工作后就不难知道,直接将LeakCanary应用于线上会有如下少量问题:
1.每次内存泄漏以后,都会生成一个.hprof文件,而后解析,并将结果写入.hprof.result。添加手机负担,引起手机卡慢等问题。
2.屡次调用GC,可能会对线上性能产生影响
3.同样的泄漏问题,会重复生成 .hprof 文件,重复分析并写入磁盘。
4..hprof文件较大,信息回捞成问题。

理解了这些问题,我们可以尝试提出少量处理方案:
1.可以根据手机信息来设定一个内存阈值 M ,当已使用内存小于 M 时,假如此时有内存泄漏,只将泄漏对象的信息放入内存当中保存,不生成.hprof文件。当已使用大于 M 时,生成.hprof文件
2.当引用链路相同时,可根据实际情况去重。
3.不直接回捞.hprof文件,可以选择回捞分析的结果
4.可以尝试将已泄漏对象存储在数据库中,一个客户同一个泄漏只检测一次,减少对客户的影响

以上想法并没有经过实际验证,仅供读者参考

总结

当我们引入LeakCanary后,它就会自动安装并且开始分析内存泄漏并报警
主要分为以下几步
1.自动安装
2.检测可能泄漏的对象
3.堆快照,生成hprof文件
4.分析hprof文件
5.对泄漏进行分类并通知

本文主要梳理了LeakCanary的主要流程与文章开始提出的几个问题,假如对您有所帮助,欢迎点赞~

最后

笔者在面试前,从网上收集了少量 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,进行了复习,在此分享给大家,希望能帮助到大家学习提升,如有需要参考的可以直接去我 GitHub地址: 733gh/Android-T3 访问查阅。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【带着问题学】关于LeakCanary2.0的四个问题

发表回复