LeakCanary(4)面试题系列

news2024/11/15 14:02:26

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

问题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 {
        // 初始化 LeakCanary
        val application = context!!.applicationContext as Application
        AppWatcher.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() }
        }
    }

    // 移除未泄漏对象对应的 KeyedWeakReference
    private 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. 将分析报告持久化到 DB
    val 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, // 默认是 KeyedWeakReferenceFinder
    referenceMatchers = config.referenceMatchers, // 默认是 AndroidReferenceMatchers
    computeRetainedHeapSize = config.computeRetainedHeapSize, // 默认是 true
    objectInspectors = config.objectInspectors, // 默认是 AndroidObjectInspectors
    metadataExtractor = config.metadataExtractor // 默认是 AndroidMetadataExtractor
    )
}


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

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

参考

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1080115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试算法25:链表中的数字相加

题目 给定两个表示非负整数的单向链表&#xff0c;请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示&#xff1f;链表中的每个节点表示整数十进制的一位&#xff0c;并且头节点对应整数的最高位数而尾节点对应整数的个位数。例如&#xff0c;两个分别表示整数98…

css吸顶特效(elementui vue3官网)

效果如图&#xff1a;当浏览器滚轮在最上面的时候 没什么区别。当鼠标滚轮超出最上面高度时会有这种粒子感。吸顶遮盖下面内容 首先要 明确 css 基础属性 position: sticky;的用法。再了解 background-image: radial-gradient(transparent 1px, #fff 1px); background-size: …

Java 8 引进的一个新特性 Optional

Optional 是 Java 8 引进的一个新特性&#xff0c;通常用于缓解常见的空指针异常问题。 Brian Goetz &#xff08;Java语言设计架构师&#xff09;对Optional设计意图的原话如下&#xff1a; Optional is intended to provide a limited mechanism for library method return…

马蹄集matji oj赛(第十二次)

目录 元素共鸣 欧拉函数 欧拉函数2 小码哥的喜欢数 整数的逆 数的自我 阶乘的质因子 分数个数 质数率 数字游戏 元素共鸣 难度&#xff1a;黄金 0时间限制&#xff1a;1秒 巴占用内存&#xff1a;128M 遥远的大陆上存在着元素共鸣的机制。 建立一个一维坐标系&#x…

保护隐私与增强网络安全之网络代理技术

目录 前言 一、网络代理技术原理 二、网络代理技术类型 1. HTTP代理 2. SOCKS代理 3. DNS代理 4. 加密代理 5. 反向代理 三、网络代理技术应用 1. 加速网络访问速度 2. 绕过网络限制 3. 保护个人隐私 4. 节省带宽 5. 改善网络安全 四、网络代理技术优缺点 网络…

APK大小缩小65%,内存减少70%:如何优化Android App

APK大小缩小65&#xff05;&#xff0c;内存减少70&#xff05;&#xff1a;如何优化Android App 我们一直在努力为我们的Android应用程序构建MVP产品。在开发MVP产品后&#xff0c;我们发现需要进行应用程序优化以提高性能。经过分析&#xff0c;我们发现了以下可以改进的应用…

比特币有助减少腐败;微软 Copilot 每月赔 20 美元;AIGC 明年会“洗冷水澡”丨 RTE 开发者日报 Vol.64

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

AMEYA360分享:村田电子搭载了Onsemi公司IoT设备专用IC的新Bluetooth® Low Energy模块开始量产

近年来&#xff0c;所有远程监控、远程控制的用例均要求具备可无线连接的电池驱动IoT设备&#xff0c;而长寿命电池与安全的数据通信功能是其关键。为此&#xff0c;在IoT边缘设备的设计方面&#xff0c;最大的课题是要提高功率效率和安全性。 Type 2EG由于无线与内置微处理器两…

React 状态管理 - Mobx 入门(下)接入实战

目录 Mobx接入实战 Mobx构造复杂应用需要注意的 Mobx5 Or Mobx4 Mobx5 Mobx4 /package.json /src/routes/index.jsx /src/app.jsx /src/index.jsx /src/models/home/index.js /src/models/index.js /src/containers/home/index.jsx Mobx VS Redux Mobx接入实战 对…

RabbitMQ之延迟队列解读

目录 基本介绍 概述 为什么需要引进RabbitMQ延迟队列 应用场景 springboot代码实战 实战架构 工程概述 RabbitConfigDeal 配置类&#xff1a;创建队列及交换机并进行绑定 MessageService业务类&#xff1a;发送消息及接收消息 主启动类RabbitMq01Application&#xff1…

2023年中国医院信息系统发展现状及行业市场规模分析[图]

医院信息系统&#xff0c;亦称“医院管理信息系统”&#xff08;简称HIS&#xff09;&#xff0c;是指利用计算机软硬件技术、网络通信技术等现代化手段&#xff0c;对医院及其所属各部门的人流、物流、财流进行综合管理&#xff0c;对在医疗活动各阶段产生的数据进行采集储存、…

Lab 1: Unix utilities汇总

这个实验主要学习了常用的一些系统调用。 Lab 1: Unix utilities Boot xv6 (easy) git克隆&#xff0c;切换分支&#xff0c;qemu。根据要求进行操作即可。 $ git clone git://g.csail.mit.edu/xv6-labs-2020 $ cd xv6-labs-2020 $ git checkout util $ make qemusleep (ea…

分享一下花店制作微信小程序的步骤是什么

一、准备阶段 在准备阶段&#xff0c;花店需要完成以下任务&#xff1a; 注册微信公众平台账号&#xff1a;首先&#xff0c;花店需要注册一个微信公众平台账号&#xff0c;这个账号将用于创建和管理微信小程序。 确定小程序的功能和需求&#xff1a;花店需要根据自身的业务需…

RISC-V架构 | 飞凌嵌入式FET7110-C国产高性能核心板现货发售!

RISC-V凭借其完全开源免费且可自由修改的特性而备受国内厂商的追捧&#xff0c;在此背景下&#xff0c;飞凌嵌入式联合RISC-V国产处理器厂商赛昉科技(StarFive)基于昉惊鸿7110处理器共同推出FET7110-C核心板。 现在&#xff0c;飞凌嵌入式FET7110-C核心板&#xff08;商业级&a…

2023.10月网络优化项目实战

基础配置 sw2 <Huawei>sy Enter system view, return user view with Ctrl+Z. [Huawei]sy sw2 [sw2]vlan batch 10 20 Info: This operation may take a few seconds. Please wait for a moment...done.[sw2]int e0/0/1 [sw2-Ethernet0/0/1]port link-type access [s…

第十章-输入输出系统

Ⅰ.锁 本质是互斥操作 原因&#xff1a;针对公共资源访问时&#xff0c;临界区若不加以互斥限制&#xff0c;可能导致执行过程中突然的中断导致出现异常。 1.互斥过程 设定互斥量M为二值信号量&#xff0c;0/1&#xff0c;P-&#xff0c;V&#xff0c;现有两个进程A、B共同…

大数据flink篇之三-flink运行环境安装(一)单机Standalone安装

一、安装包下载地址 https://archive.apache.org/dist/flink/flink-1.15.0/ 二、安装配置流程 前提基础&#xff1a;Centos环境&#xff08;建议7以上&#xff09; 安装命令&#xff1a; 解压&#xff1a;tar -zxvf flink-xxxx.tar.gz 修改配置conf/flink-conf.yaml&#xff1…

最新AI创作系统源码ChatGPT网站源码/支持Midjourney,AI绘画/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

RxJava介绍及基本原理

随着互联网的迅猛发展&#xff0c;Java已成为最广泛应用于后端开发的语言之一。而在处理异步操作和事件驱动编程方面&#xff0c;传统的Java多线程并不总是最佳选择。这时候&#xff0c;RxJava作为一个基于观察者模式、函数式编程和响应式编程理念的库&#xff0c;为我们提供了…

【Nuget】程序包源

程序包源地址(部分) Azure 中国区的官方 NuGet 程序包源地址 https://nuget.cdn.azure.cn/v3/index.json 官方 NuGet 程序包源地址 V2 https://www.nuget.org/api/v2 官方 NuGet 程序包源地址 V3 https://api.nuget.org/v3/index.json MyGet 上 Eto.Forms 框架的程序包源地址 h…