LeakCanary(4)面试题系列

2023-10-10 04:36
文章标签 面试题 系列 leakcanary

本文主要是介绍LeakCanary(4)面试题系列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

问题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可以自定义那些配置?

// Java 语法
LeakCanary.Config config = LeakCanary.getConfig().newBuilder().retainedVisibleThreshold(3).build();
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 执行器,示例程序如下:

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

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

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

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

internal class MainProcessAppWatcherInstaller : ContentProvider() {override fun onCreate(): Boolean {// 初始化 LeakCanaryval application = context!!.applicationContext as ApplicationAppWatcher.manualInstall(application)return true}...
}

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

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

  • 初始化 LeakCanary 内部分析引擎;
  • 在 Android Framework 上注册五种 Android 泄漏场景的监控。
fun manualInstall(application: Application,retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {checkMainThread()...// 初始化 InternalLeakCanary 内部引擎 (已简化为等价代码,后文会提到)InternalLeakCanary(application)// 注册五种 Android 泄漏场景的监控 Hook 点watchersToInstall.forEach {it.install()}
}fun appDefaultWatchers(application: Application,reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {// 对应 5 种 Android 泄漏场景(后文具体分析)return listOf(ActivityWatcher(application, reachabilityWatcher),FragmentAndViewModelWatcher(application, reachabilityWatcher),RootViewWatcher(reachabilityWatcher),ServiceWatcher(reachabilityWatcher))
}

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

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

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

class ObjectWatcher constructor(private val clock: Clock,private val checkRetainedExecutor: Executor,private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {if (!isEnabled()) {// 监控开关return}// 被监控的对象映射表 <UUID,KeyedWeakReference>private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()// KeyedWeakReference 关联的引用队列,用于判断对象是否泄漏private val queue = ReferenceQueue<Any>()// 1. 为 watchedObject 对象增加监控@Synchronized override fun expectWeaklyReachable(watchedObject: Any,description: String) {// 1.1 移除 watchedObjects 中未泄漏的引用对象removeWeaklyReachableObjects()// 1.2 新建一个 KeyedWeakReference 引用对象val key = UUID.randomUUID().toString()val watchUptimeMillis = clock.uptimeMillis()watchedObjects[key] = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)// 2. 五秒后检查引用对象是否出现在引用队列中,否则判定发生泄漏// checkRetainedExecutor 相当于 postDelay 五秒后执行 moveToRetained() 方法checkRetainedExecutor.execute {moveToRetained(key)}}// 2. 五秒后检查引用对象是否出现在引用队列中,否则说明发生泄漏@Synchronized private fun moveToRetained(key: String) {// 2.1 移除 watchedObjects 中未泄漏的引用对象removeWeaklyReachableObjects()// 2.2 依然存在的引用对象被判定发生泄漏val retainedRef = watchedObjects[key]if (retainedRef != null) {retainedRef.retainedUptimeMillis = clock.uptimeMillis()// 3. 回调通知 LeakCanary 内部处理onObjectRetainedListeners.forEach { it.onObjectRetained() }}}// 移除未泄漏对象对应的 KeyedWeakReferenceprivate fun removeWeaklyReachableObjects() {var ref: KeyedWeakReference?do {ref = queue.poll() as KeyedWeakReference?if (ref != null) {// KeyedWeakReference 出现在引用队列中,说明未发生泄漏watchedObjects.remove(ref.key)}} while (ref != null)}// 4. Heap Dump 后移除所有监控时间早于 heapDumpUptimeMillis 的引用对象@Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {val weakRefsToRemove = watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }weakRefsToRemove.values.forEach { it.clear() }watchedObjects.keys.removeAll(weakRefsToRemove.keys)}// 获取是否有内存泄漏对象val hasRetainedObjects: Boolean@Synchronized get() {// 移除 watchedObjects 中未泄漏的引用对象removeWeaklyReachableObjects()return watchedObjects.any { it.value.retainedUptimeMillis != -1L }}// 获取内存泄漏对象计数val retainedObjectCount: Int@Synchronized get() {// 移除 watchedObjects 中未泄漏的引用对象removeWeaklyReachableObjects()return watchedObjects.count { it.value.retainedUptimeMillis != -1L }}
}

问题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

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

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

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

参考

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

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

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

这篇关于LeakCanary(4)面试题系列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/177997

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

荣耀嵌入式面试题及参考答案

在项目中是否有使用过实时操作系统? 在我参与的项目中,有使用过实时操作系统。实时操作系统(RTOS)在对时间要求严格的应用场景中具有重要作用。我曾参与的一个工业自动化控制项目就采用了实时操作系统。在这个项目中,需要对多个传感器的数据进行实时采集和处理,并根据采集到的数据及时控制执行机构的动作。实时操作系统能够提供确定性的响应时间,确保关键任务在规定的时间内完成。 使用实时操作系统的

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

一些其他面试题

阿里二面:那你来说说定时任务?单机、分布式、调度框架下的定时任务实现是怎么完成的?懵了。。_哔哩哔哩_bilibili 1.定时算法 累加,第二层每一个格子是第一层的总时间400 ms= 20 * 20ms 2.MQ消息丢失 阿里二面:高并发场景下引进消息队列有什么问题?如何保证消息只被消费一次?真是捏了一把汗。。_哔哩哔哩_bilibili 发送消息失败

GPT系列之:GPT-1,GPT-2,GPT-3详细解读

一、GPT1 论文:Improving Language Understanding by Generative Pre-Training 链接:https://cdn.openai.com/research-covers/languageunsupervised/language_understanding_paper.pdf 启发点:生成loss和微调loss同时作用,让下游任务来适应预训

zookeeper相关面试题

zk的数据同步原理?zk的集群会出现脑裂的问题吗?zk的watch机制实现原理?zk是如何保证一致性的?zk的快速选举leader原理?zk的典型应用场景zk中一个客户端修改了数据之后,其他客户端能够马上获取到最新的数据吗?zk对事物的支持? 1. zk的数据同步原理? zk的数据同步过程中,通过以下三个参数来选择对应的数据同步方式 peerLastZxid:Learner服务器(Follo

java常用面试题-基础知识分享

什么是Java? Java是一种高级编程语言,旨在提供跨平台的解决方案。它是一种面向对象的语言,具有简单、结构化、可移植、可靠、安全等特点。 Java的主要特点是什么? Java的主要特点包括: 简单性:Java的语法相对简单,易于学习和使用。面向对象:Java是一种完全面向对象的语言,支持封装、继承和多态。跨平台性:Java的程序可以在不同的操作系统上运行,称为"Write once,

Java基础回顾系列-第七天-高级编程之IO

Java基础回顾系列-第七天-高级编程之IO 文件操作字节流与字符流OutputStream字节输出流FileOutputStream InputStream字节输入流FileInputStream Writer字符输出流FileWriter Reader字符输入流字节流与字符流的区别转换流InputStreamReaderOutputStreamWriter 文件复制 字符编码内存操作流(