• LeakCanary(4)面试题系列


    序、慢慢来才是最快的方法。

    背景

    LeakCanary是Square的开源库,通过弱引用方式侦查Activity或Fragment对象的生命周期,若发现内存泄漏自动 dump Hprof文件,通过HAHA库得到泄露的最短路径,最后通过Notification展示。

    简单说就是在在Activity对象onDestory的时候,新建一个WeakReference对象指向Activity对象,如果Activity对象被垃圾回收的话,WeakReference对象就会进入引用序列的ReferenceQueue。

    所以我们只需要在Activity对象OnDestory之后去查看ReferenceQueue序列是否有该WeakReference对象即可。

    第一次观察是Activity的onDestory5秒后,如果发现ReferenceQueue对来还没有WeakReference对象,就进入第二次观察,如果有了,就证明没有泄漏,第二次观察跟第一次观察相比区别在于会先进行垃圾回收,在进行ReferenceQueue序列的观察。
     

    问题1:LeakCanary 支持Android 场景中的那些内存泄漏监测?

    1. 已销毁的 Activity 对象(进入 DESTROYED 状态);
    2. 已销毁的 Fragment 对象和 Fragment View 对象(进入 DESTROYED 状态);
    3. 已清除的的 ViewModel 对象(进入 CLEARED 状态);
    4. 已销毁的的 Service 对象(进入 DESTROYED 状态);
    5. 已从 WindowManager 中移除的 RootView 对象;

    问题2:LeakCanary 怎么实现内存泄漏监控?

    LeakCanary 通过以下 2 点实现内存泄漏监控:

    • 1.在 Android Framework 中注册无用对象监听: 通过全局监听器或者 Hook 的方式,在 Android Framework 上监听 Activity 和 Service 等对象进入无用状态的时机(例如在 Activity#onDestroy() 后,产生一个无用 Activity 对象);
    • 2.利用引用对象可感知对象垃圾回收的机制判定内存泄漏: 为无用对象包装弱引用,并在一段时间后(默认为五秒)观察弱引用是否如期进入关联的引用队列,是则说明未发生泄漏,否则说明发生泄漏(无用对象被强引用持有,导致无法回收,即泄漏)。

    问题3:LeakCanary可以自定义那些配置?

    1. // Java 语法
    2. LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
    3. .retainedVisibleThreshold(3)
    4. .build();
    5. LeakCanary.setConfig(config);

    以下用一个表格总结 LeakCanary 主要的配置项:

    问题4:如何加快dump速度?

    使用快手 Koom 加快 Dump 速度。

    eakCanary 默认的 Java Heap Dump 使用的是 Debug.dumpHprofData() ,在 Dump 的过程中会有较长时间的应用冻结时间。 快手技术团队在开源框架 Koom 中提出了优化方案:利用 Copy-on-Write 思想,fork 子进程再进行 Heap Dump 操作。

    LeakCanary 配置项可以修改 Heap Dump 执行器,示例程序如下:

    1. // 依赖:
    2. debugImplementation "com.kuaishou.koom:koom-java-leak:2.2.0"
    3. // 使用默认配置初始化 Koom
    4. DefaultInitTask.init(application)
    5. // 自定义 LeakCanary 配置
    6. LeakCanary.config = LeakCanary.config.copy(
    7. // 自定义 Heap Dump 执行器
    8. heapDumper = {
    9. ForkJvmHeapDumper.getInstance().dump(it.absolutePath)
    10. }
    11. )

    问题5:LeakCanary 如何实现自动初始化?

    旧版本的 LeakCanary 需要在 Application 中调用相关初始化 API,而在 LeakCanary v2 版本中却不再需要手动初始化,为什么呢?—— 这是因为 LeakCanary 利用了 ContentProvider 的初始化机制来间接调用初始化 API。

    ContentProvider 的常规用法是提供内容服务,而另一个特殊的用法是提供无侵入的初始化机制,这在第三方库中很常见,Jetpack 中提供的轻量级初始化框架 App Startup 也是基于 ContentProvider 的方案。

    1. internal class MainProcessAppWatcherInstaller : ContentProvider() {
    2. override fun onCreate(): Boolean {
    3. // 初始化 LeakCanary
    4. val application = context!!.applicationContext as Application
    5. AppWatcher.manualInstall(application)
    6. return true
    7. }
    8. ...
    9. }

    问题6:LeakCanary 初始化过程分析。

    LeakCanary 的初始化工程可以概括为 2 项内容:

    • 初始化 LeakCanary 内部分析引擎;
    • 在 Android Framework 上注册五种 Android 泄漏场景的监控。
    1. fun manualInstall(
    2. application: Application,
    3. retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    4. watchersToInstall: List = appDefaultWatchers(application)
    5. ) {
    6. checkMainThread()
    7. ...
    8. // 初始化 InternalLeakCanary 内部引擎 (已简化为等价代码,后文会提到)
    9. InternalLeakCanary(application)
    10. // 注册五种 Android 泄漏场景的监控 Hook 点
    11. watchersToInstall.forEach {
    12. it.install()
    13. }
    14. }
    15. fun appDefaultWatchers(
    16. application: Application,
    17. reachabilityWatcher: ReachabilityWatcher = objectWatcher
    18. ): List {
    19. // 对应 5 种 Android 泄漏场景(后文具体分析)
    20. return listOf(
    21. ActivityWatcher(application, reachabilityWatcher),
    22. FragmentAndViewModelWatcher(application, reachabilityWatcher),
    23. RootViewWatcher(reachabilityWatcher),
    24. ServiceWatcher(reachabilityWatcher)
    25. )
    26. }

    问题7: LeakCanary 如何判定对象泄漏?

    在以上步骤中,当对象的使用生命周期结束后,会交给 ObjectWatcher 监控,现在我们来具体看下它是怎么判断对象发生泄漏的。主要逻辑概括为 3 步:

    • 第 1 步: 为被监控对象 watchedObject 创建一个 KeyedWeakReference 弱引用,并存储到 的映射表中;
    • 第 2 步: postDelay 五秒后检查引用对象是否出现在引用队列中,出现在队列则说明被监控对象未发生泄漏。随后,移除映射表中未泄露的记录,更新泄漏的引用对象的 retainedUptimeMillis 字段以标记为泄漏;
    • 第 3 步: 通过回调 onObjectRetained 告知 LeakCanary 内部发生新的内存泄漏。
    1. val objectWatcher = ObjectWatcher(
    2. // lambda 表达式获取当前系统时间
    3. clock = { SystemClock.uptimeMillis() },
    4. // lambda 表达式实现 Executor SAM 接口
    5. checkRetainedExecutor = {
    6. mainHandler.postDelayed(it, retainedDelayMillis)
    7. },
    8. // lambda 表达式获取监控开关
    9. isEnabled = { true }
    10. )

    1. class ObjectWatcher constructor(
    2. private val clock: Clock,
    3. private val checkRetainedExecutor: Executor,
    4. private val isEnabled: () -> Boolean = { true }
    5. ) : ReachabilityWatcher {
    6. if (!isEnabled()) {
    7. // 监控开关
    8. return
    9. }
    10. // 被监控的对象映射表
    11. private val watchedObjects = mutableMapOf()
    12. // KeyedWeakReference 关联的引用队列,用于判断对象是否泄漏
    13. private val queue = ReferenceQueue()
    14. // 1. 为 watchedObject 对象增加监控
    15. @Synchronized
    16. override fun expectWeaklyReachable(
    17. watchedObject: Any,
    18. description: String
    19. ) {
    20. // 1.1 移除 watchedObjects 中未泄漏的引用对象
    21. removeWeaklyReachableObjects()
    22. // 1.2 新建一个 KeyedWeakReference 引用对象
    23. val key = UUID.randomUUID().toString()
    24. val watchUptimeMillis = clock.uptimeMillis()
    25. watchedObjects[key] = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    26. // 2. 五秒后检查引用对象是否出现在引用队列中,否则判定发生泄漏
    27. // checkRetainedExecutor 相当于 postDelay 五秒后执行 moveToRetained() 方法
    28. checkRetainedExecutor.execute {
    29. moveToRetained(key)
    30. }
    31. }
    32. // 2. 五秒后检查引用对象是否出现在引用队列中,否则说明发生泄漏
    33. @Synchronized
    34. private fun moveToRetained(key: String) {
    35. // 2.1 移除 watchedObjects 中未泄漏的引用对象
    36. removeWeaklyReachableObjects()
    37. // 2.2 依然存在的引用对象被判定发生泄漏
    38. val retainedRef = watchedObjects[key]
    39. if (retainedRef != null) {
    40. retainedRef.retainedUptimeMillis = clock.uptimeMillis()
    41. // 3. 回调通知 LeakCanary 内部处理
    42. onObjectRetainedListeners.forEach { it.onObjectRetained() }
    43. }
    44. }
    45. // 移除未泄漏对象对应的 KeyedWeakReference
    46. private fun removeWeaklyReachableObjects() {
    47. var ref: KeyedWeakReference?
    48. do {
    49. ref = queue.poll() as KeyedWeakReference?
    50. if (ref != null) {
    51. // KeyedWeakReference 出现在引用队列中,说明未发生泄漏
    52. watchedObjects.remove(ref.key)
    53. }
    54. } while (ref != null)
    55. }
    56. // 4. Heap Dump 后移除所有监控时间早于 heapDumpUptimeMillis 的引用对象
    57. @Synchronized
    58. fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
    59. val weakRefsToRemove = watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
    60. weakRefsToRemove.values.forEach { it.clear() }
    61. watchedObjects.keys.removeAll(weakRefsToRemove.keys)
    62. }
    63. // 获取是否有内存泄漏对象
    64. val hasRetainedObjects: Boolean
    65. @Synchronized get() {
    66. // 移除 watchedObjects 中未泄漏的引用对象
    67. removeWeaklyReachableObjects()
    68. return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
    69. }
    70. // 获取内存泄漏对象计数
    71. val retainedObjectCount: Int
    72. @Synchronized get() {
    73. // 移除 watchedObjects 中未泄漏的引用对象
    74. removeWeaklyReachableObjects()
    75. return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
    76. }
    77. }

    问题8:LeakCanary 发现泄漏对象后就会触发分析吗?

    ObjectWatcher 判定被监控对象发生泄漏后,会通过接口方法 OnObjectRetainedListener#onObjectRetained() 回调到 LeakCanary 内部的管理器 InternalLeakCanary 处理(在前文 AppWatcher 初始化中提到过)。LeakCanary 不会每次发现内存泄漏对象都进行分析工作,而会进行两个拦截:

    • 拦截 1:泄漏对象计数未达到阈值,或者进入后台时间未达到阈值;
    • 拦截 2:计算距离上一次 HeapDump 未超过 60s。

    问题8:LeakCanary 在哪个线程分析堆快照?

    在前面的工作中,LeakCanary 已经成功生成 .hprof 堆快照文件,并且发送了一个 LeakCanary 内部事件 HeapDump。那么这个事件在哪里被消费的呢?

    一步步跟踪代码可以看到 LeakCanary 的配置项中设置了多个事件消费者 EventListener,其中与 HeapDump 事件有关的是 when{} 代码块中三个消费者。不过,这三个消费者并不是并存的,而是会根据 App 当前的依赖项而选择最优的执行策略:

    • 策略 1 - WorkerManager 多进程分析
    • 策略 2 - WorkManager 异步分析
    • 策略 3 - 异步线程分析(兜底策略)

    问题9:LeakCanary 如何分析堆快照?

    在前面的分析中,我们已经知道 LeakCanary 是通过子线程或者子进程执行 AndroidDebugHeapAnalyzer.runAnalysisBlocking 方法来分析堆快照的,并在分析过程中和分析完成后发送回调事件。

    现在我们来阅读 LeakCanary 的堆快照分析过程:

    AndroidDebugHeapAnalyzer.kt

    1. fun runAnalysisBlocking(
    2. heapDumped: HeapDump,
    3. isCanceled: () -> Boolean = { false },
    4. progressEventListener: (HeapAnalysisProgress) -> Unit
    5. ): HeapAnalysisDone<*> {
    6. ...
    7. // 1. .hprof 文件
    8. val heapDumpFile = heapDumped.file
    9. // 2. 分析堆快照
    10. val heapAnalysis = analyzeHeap(heapDumpFile, progressListener, isCanceled)
    11. val analysisDoneEvent = ScopedLeaksDb.writableDatabase(application) { db ->
    12. // 3. 将分析报告持久化到 DB
    13. val id = HeapAnalysisTable.insert(db, heapAnalysis)
    14. // 4. 发送分析完成事件(返回到上一级进行发送:InternalLeakCanary.sendEvent(doneEvent))
    15. val showIntent = LeakActivity.createSuccessIntent(application, id)
    16. val leakSignatures = fullHeapAnalysis.allLeaks.map { it.signature }.toSet()
    17. val leakSignatureStatuses = LeakTable.retrieveLeakReadStatuses(db, leakSignatures)
    18. val unreadLeakSignatures = leakSignatureStatuses.filter { (_, read) -> !read}.keys.toSet()
    19. HeapAnalysisSucceeded(heapDumped.uniqueId, fullHeapAnalysis, unreadLeakSignatures ,showIntent)
    20. }
    21. return analysisDoneEvent
    22. }
    1. private fun analyzeHeap(
    2. heapDumpFile: File,
    3. progressListener: OnAnalysisProgressListener,
    4. isCanceled: () -> Boolean
    5. ): HeapAnalysis {
    6. ...
    7. // Shark 堆快照分析器
    8. val heapAnalyzer = HeapAnalyzer(progressListener)
    9. ...
    10. // 构建对象图信息
    11. val sourceProvider = ConstantMemoryMetricsDualSourceProvider(ThrowingCancelableFileSourceProvider(heapDumpFile)
    12. val graph = sourceProvider.openHeapGraph(proguardMapping = proguardMappingReader?.readProguardMapping())
    13. ...
    14. // 开始分析
    15. heapAnalyzer.analyze(
    16. heapDumpFile = heapDumpFile,
    17. graph = graph,
    18. leakingObjectFinder = config.leakingObjectFinder, // 默认是 KeyedWeakReferenceFinder
    19. referenceMatchers = config.referenceMatchers, // 默认是 AndroidReferenceMatchers
    20. computeRetainedHeapSize = config.computeRetainedHeapSize, // 默认是 true
    21. objectInspectors = config.objectInspectors, // 默认是 AndroidObjectInspectors
    22. metadataExtractor = config.metadataExtractor // 默认是 AndroidMetadataExtractor
    23. )
    24. }

    可以看到,堆快照分析最终是交给 Shark 中的 HeapAnalizer 完成的,核心流程是:

    • 1、在堆快照中寻找泄漏对象,默认是寻找 KeyedWeakReference 类型对象;
    • 2、分析 KeyedWeakReference 对象的最短引用链,并按照引用链签名分组,按照 Application Leaks 和 Library Leaks 分类;
    • 3、返回分析完成事件。

    参考

    Android 开源库 #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!

    被问到:如何检测线上内存泄漏,通过 LeakCanary 探究!
    快手KOOM高性能线上解决方案

    04 | 内存优化(下):内存优化这件事,应该从哪里着手?

  • 相关阅读:
    C++ 类和对象篇(三) 空类和6个默认成员函数
    打印星堆(for循环嵌套实例)
    centos执行systemctl restart命令报连接超时
    上周热点回顾(7.3-7.9)
    类图 UML从入门到放弃系列之二
    重点速看,超全面汇编?成都市关于加快发展先进制造业实现工业转型升级发展若干政策的申报条件、时间、材料和奖励
    Linux下大文件切割与合并
    VsFtpd的环境搭建,虚拟登录,Linux服务器
    基于微前端qiankun的多页签缓存方案实践
    ChatGLM系列五:Lora微调
  • 原文地址:https://blog.csdn.net/qq_37492806/article/details/133716579