手写一个IO泄露监测框架

news/2024/5/5 12:26:26/文章来源:https://blog.csdn.net/maniuT/article/details/130131544

作者:长安皈故里

大家好,最近由于项目原因,对IO资源泄漏的监测进行了一番调研深入了解,发现IO泄漏监测框架实现成本比较低,效果很显著;同时由于IO监测涉及到反射,还了解到了通过一种巧妙的方式实现Android P以上非公开api的访问。

接下来本篇文章首先会带你了解一些前置知识,然后会带领从0到1手把手教你搭建一个IO泄漏监测框架。

一. 为什么要做IO泄漏检测?

IO一般就是指的常见的文件流读写、数据库读写,相信每个人都知道,完成读写后都应该手动调用流的close() 方法关闭,一旦忘记就引起了io泄漏了

如果项目中这种问题场景比较多,就会导致fd无节制的增加,导致应用内存紧张,严重甚至引发OOM,非常影响用户体验。

为了避免操作完读写流忘记close,java和kotlin两种编程语言分别给我们提供了以下语法糖:

1. 实现java的AutoCloseable并搭配try-with-resource

看一段常见的代码:

public static void main(String[] args) {try (FileInputStream fis = new FileInputStream(new File("test.txt"))) {byte[] data = new byte[1024];int read = fis.read(data);//执行其他操纵} catch (Exception e) {e.printStackTrace();}
}

FileInputStream实现了AutoCloseable接口,并重写了接口的close()方法,通过上面的try-with-resource语法,我们就不需要显示调用close方法关闭io,java会自动帮助我们完成这个操作:

常见的InputStream、OutputStream 、Scanner 、PrintWriter都实现了AutoCloseable接口,所以文件读写时可以非常方便的使用上面的语法糖。

2. 使用kotlin中的use()扩展

kotlin针对Closeable(实现了AutoCloseable)接口提供了下面的扩展:

我们常见的InputStream、OutputStream 、Scanner 、PrintWriter等都是支持这个扩展函数的:

override fun create(context: Context) {FileInputStream("").use {//执行某些操作}
}

虽然kotlin和java都从语言层面上帮助尽可能我们读写io流实现安全关闭,但是真正到写代码时忘了是真的忘了;而且项目中还可能存在历史代码也忘记了关闭流,查找起来也是毫无头绪的。

面对上面这中情况,就需要一种io泄漏的检测机制,不管是针对项目的历史代码还是新写的代码,能够检测文件流是否关闭,没有关闭则获取流创建的堆栈并上报帮助开发定位问题,接下来我们来一步步的实现这种能力吧。

二. IO泄漏检测的实现思路

头脑风暴一下,想要检测流有没有关闭,关键就是检测诸如FileInputStream等操作文件流的类close方法有没有调用;那什么时机才应该去检测呢,当FileInputStream等流类准备销毁的时候就可以去检测了,而类销毁的时候会调用finalize()方法(PS:暂时不考虑finalize()特殊场景下的表现,这里认为都会被正常执行),所以检测的最佳时机就是在流类的finalize() 方法执行的时候

经过上面的分析,我们可以写出下面的代码:

public class FileInputStream {private Object flag = null;public void open() {//打开文件流时赋值flag = "open";}public void close() throws Exception {//关闭文件流置空flag = null;}@Overrideprotected void finalize() throws Throwable {super.finalize();//flag等于null,说明忘记执行close方法关闭流,io泄漏if (flag != null) {Throwable throwable = new Throwable("io leak");//执行异常日志的打印,或者回调给外部。//兜底流的关闭close();}}
}

代码中有非常详细的注释,这里就不再一一进行讲述。

所以如果能在我们常见的FileInputStreamFileOutputStreamRandomAccessFile等流类中也增加上面的代码,io泄漏监测这不就成了!!

Android官方自然也能够想到,并且还干了,常见的官方流类FileInputStream FileOutputStream RandomAccessFile CursorWindow等都增加了上面类似监控逻辑,接下来我们以FileInputStream为例进行分析。

三 瞅瞅官方FileInputStream源码

这里我们先提前说下,官方监控流类是否泄漏,并不是直接在里面增加逻辑代码,想想也是,那么多流类,一个个增加过去导致模板代码太多,不如封装一个工具类供各个流类使用,这里的工具类就是CloseGuard

说清了上面,我们就看下FileInputStream的源码:

1. 获取工具类CloseGuard

由于CloseGuard的源码无法直接在AS中查看,这里我们借助 aospxref.com/android-12.… 网站查看下该类的源码:

CloseGuard.get()方法就是创建了一个CloseGuard对象。

2. 打开文件流

FileInputStream构造方法主要干了两件事情:

  • 通过传入的文件路径调用IoBridge.open()打开文件流(这个底层最终会调用了open(const char *pathname,int flags,mode_t mode),做io监控时一般需要hook该方法)。

  • 同时还会调用CloseGuard.open()方法:

这个方法主要干的事情就是创建了一个Throwable对象,获取当前流创建的堆栈,并赋值给CloseGuardcloserNameOrAllocationInfo字段。


3. 关闭文件流

FileInputStreamclose()方法主要干了两件事:

  • 调用CloseGuardclose()方法:

很简单,就是将上面赋值的closerNameOrAllocationInfo字段重新置空。

  • 关闭文件流;

4. 重写finalize()监控FileInputStream的销毁

FileInputStreamfinalize()方法主要干了两件事:

  • 调用CloseGuardwarnIfOpen()方法:

如果closerNameOrAllocationInfo字段不为空,说明FileInputStreamclose() 关闭文件流的方法漏了调用,发生了io泄漏,调用reporter.report() 方法并传入closerNameOrAllocationInfo参数(这个参数上面有说:保存了流创建时的堆栈,一旦获取到我们就能很快知道哪个地方创建的流发生了泄漏)。

  • 兜底关闭流;

通过上面的分析可以得知,一旦发生io泄漏,就会通过reporter.report() 上报,这就是我们监控应用整体io泄漏的关键。

看下reporter是个啥:

reporter是一个静态变量,本质上是一个实现了Reporter接口的默认实现类DefaultReporter ,默认通过report() 方法打印io泄漏的系统日志。

同时外部可以注入自定义的实现了Reporter接口的类:

讲到这里大家是不是明白了,如果实现应用层的io泄漏检测,只要我们通过动态代理+反射代理掉reporter这个静态变量,替换成我们自定义实现的Reporter接口的类,并在自定义类中实现io泄漏异常上报的逻辑,不就完美实现监听了吗!!

想象很美好,现实很残酷,CloseGuard是个系统类,且被@hide隐藏,同时上面的setReporter()方法被@UnsupportedAppUsage注解,所以这个是官方非公开的api。在Android P以下自然可以通过反射调用,但是在Android P及以上使用反射就会报错,所以还得探索一种高版本能够成功反射系统非公开api的方法。

四. Android P及以上非公开api访问的实现

想要访问系统非公开api,那就只有系统api才能调用,一般有两种方式:

  1. 将我们自己的类的classloader转换为系统的classloader去调用系统非公开api;
  2. 借助于系统类方法去调用系统非公开api,即双反射实现机制;

这里我们采用的是第二种双反射实现方式,并且weishu大佬提供了一个github库方面我们拿来使用:

dependencies {implementation 'com.github.tiann:FreeReflection:3.1.0'
}

然后在Application.attachBaseContext()方法中调用;

@Override
protected void attachBaseContext(Context base) {super.attachBaseContext(base);Reflection.unseal(base);
}

五. 从0到1搭建IO泄露监测框架

上面的准备知识都讲解完毕了,接下来我们从0到1开始我们的io泄漏检测框架搭建之旅吧。

1. 创建名称为ResourceLeakCanary的一个module,并引入下面两个依赖

dependencies {implementation 'com.github.tiann:FreeReflection:3.1.0'implementation("androidx.startup:startup-runtime:1.1.1")
}

2. 通过startup实现SDK的自动初始化,并借助FreeReflection库解除系统非公开api访问限制

class IOLeakCanaryInstall : Initializer<Unit> {override fun create(context: Context) {//android p及以上非公开api允许调用Reflection.unseal(context)//初始化核心io泄漏监测IOLeakCanaryCore().init(context.applicationContext)Log.i(IOLeakCanaryCore.TAG, "IOLeakCanaryInstall install success!")}override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}

3. 创建IOLeakCanaryCore,里面实现核心的hook CloseGuard#Reporter的逻辑

class IOLeakCanaryCore {companion object {const val TAG = "IOLeakCanary"lateinit var APPLICATION: Context}/*** CloseGuard原始的Reporter接口实现类DefaultReporter*/private var mOriginalReporter: Any? = nullfun init(application: Context) {APPLICATION = applicationval hookResult = tryHook()Log.i(TAG, "init: hookResult = $hookResult")}@SuppressLint("SoonBlockedPrivateApi")private fun tryHook(): Boolean {try {val closeGuardCls = Class.forName("dalvik.system.CloseGuard")val closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter")//拿到CloseGuard原始的Reporter接口实现类DefaultReporterval methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter")mOriginalReporter = methodGetReporter.invoke(null)//获取setReporter的Method实例,便于后续反射该方法注入我们自定义的Report对象val methodSetReporter =closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls)//将CloseGuard的stackAndTrackingEnabled字段置为true,否则为false将不会调用自定义的Reporter对象val methodSetEnabled =closeGuardCls.getDeclaredMethod("setEnabled", Boolean::class.java)methodSetEnabled.invoke(null, true)//借助动态代理+反射注入我们自定义的Report对象val classLoader = closeGuardReporterCls.classLoader ?: return falsemethodSetReporter.invoke(null,Proxy.newProxyInstance(classLoader,arrayOf(closeGuardReporterCls),IOLeakReporter()))return true} catch (e: Throwable) {Log.e(TAG, "tryHook error: message = ${e.message}")}return false}/*** 拦截report并收集堆栈*/inner class IOLeakReporter : InvocationHandler {override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {if (method?.name == "report") {//io泄漏,收集堆栈并上报,其中args[1]就代表着上面的//CloseGuard#closerNameOrAllocationInfo字段,保存了流打开时的堆栈详细val stack = args?.get(1) as? Throwable ?: return nullval stackTraceToString = stackTraceToString(stack.stackTrace)//这里只是通过日志进行打印,有需要的可以定制这块逻辑,比如加入异常上报机制Log.i(TAG, "IOLeakReporter: invoke report = $stackTraceToString")return null}return method?.invoke(mOriginalReporter, args)}/*** 处理堆栈*/private fun stackTraceToString(arr: Array<StackTraceElement>?): String {val stacks = arr?.toMutableList()?.take(8) ?: return ""val sb = StringBuffer(stacks.size)for (stackTraceElement in stacks) {sb.append(stackTraceElement.toString()).appendLine()}return sb.toString()}}
}

类上面有非常丰富的注释,我这里就不再进行一一讲解,大家仔细阅读下上面的代码自然会明白。

以上就是全部的代码了,总共也就100行左右,我们可以在上面的IOLeakReporterinvoke方法中对于io泄漏接入告警机制,非常适合在debug环境下进行对项目进行一个全面的io泄漏检测。代码写完了,接下来我们就做一个测试吧。

4. io泄漏检测测试

我们写一段测试代码,获取cpu相关详细,并且故意不释放文件流:

运行下项目,查看logcat日志输出:

可以看到有告警日志打印,并通过日志直接就定位到了异常逻辑:代码第35行创建的FileInputStream流使用完之后没有被关闭,这样我们就可以很快去修复了。

六. 总结

其实,如果了解过matrix-io-canary源码的人,应该很快就可以发现,这不就是matrix-io-canary中io泄漏监测的实现源码吗! 笔者只是在通读了matrix-io-canary之后,通过整理涉及到的相关知识点,以一种更加通俗的方式进行了讲解,希望本篇文章能对你有所帮助。

不过请注意,以上CloseGuard是基于Android12的源码进行的分析,不同的系统版本比如Android8实现是不同的;而且涉及到系统非公开api的访问也是借助了FreeReflection进行了实现,本身Android官方是禁止使用这些非公开api的,所以为了应用的稳定性,建议大家只在debug环境下使用上述逻辑


Android 核心知识点

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
音视频面试题锦:https://qr18.cn/AcV6Ap

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通达信欧奈尔RPS指标公式详解

RPS相对强度指标&#xff0c;是国内的投资者根据威廉欧奈尔所著书籍《笑傲股市》中的RS评级改进的。 根据书中介绍&#xff1a; RS评级衡量了某一给定股票在过去52周内相对股市中其他股票的表现。市场上每一只股票都被指定了1~99范围内的某一数值&#xff0c;99代表相对强度最高…

YOLOV7运行步骤(推理、训练全过程)

下载源代码&#xff1a;点击下载 执行以下命令安装requirements.txt中的相关依赖 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple官网下载权重yolov7.pt&#xff08;测试使用&#xff09;、yolov7-tiny.pt&#xff08;训练使用&#xff0c;这里…

10 JS01——初识JS

目标&#xff1a; 1、初识JavaScript 2、JavaScript注释 3、JavaScript输入输出语句 一、初识JavaScript 1、JavaScript是什么 JavaScript是世界上最流行的语言之一&#xff0c;是一种运行在客户端的脚本语言(Script是脚本的意思) 脚本语言:不需要编译&#xff0c;运行过程…

Vue2-黑马(九)

0目录&#xff1a; &#xff08;1&#xff09;router-动态菜单 &#xff08;2&#xff09;vuex-入门 &#xff08;3&#xff09;vuex-mapState &#xff08;1&#xff09;router-动态菜单 我们点击按钮跳转到主页面&#xff0c;主页在制作动态菜单&#xff0c;路由的跳转方…

keil代码格式化快捷方法

美化当前文件&#xff1a;-n !E --styleansi -p -s4 -S -f -xW -w -xw 美化整个工程文件&#xff1a;-n "$E*.c" "$E*.h" --styleansi -p -s4 -S -f -xW -w -xw -R 当前时间&#xff1a;!E~E^E 添加文件注释&#xff1a;!E 函数功能注释&#xff1a;!E ~…

快排(动图详细版,快速理解)

注&#xff1a;本文主要介绍六大排序中的快排 文章目录前言一、三大法则1.1 Hoare法1.2 挖坑法1.3 双指针法&#xff08;更加便捷&#xff09;1.4 三种方法时间复杂度计算二、快排栈问题优化方式2.1 三数取中2.2 小区间优化三、非递归快排前言 快速排序是Hoare于1962年提出的一…

Linux高并发服务器(webserver)

一.有限状态机 它的转移函数表示系统从一个状态转移到另一个状态的条件 二.EPOLL 在内核中创建一个数据&#xff0c;这个数据有两个比较重要的数据&#xff0c;一个是需要检测的文件描述符的信息&#xff08;红黑树&#xff09;&#xff0c;一个双向链表&#xff0c;存放检测到…

利用多专家模型解决长尾识别任务

来源&#xff1a;投稿 作者&#xff1a;TransforMe 编辑&#xff1a;学姐 贡献 提出了RoutIng Diverse Experts&#xff08;RIDE&#xff09;&#xff0c;不仅可以减少所有类别的variance&#xff0c;并且还可以减少尾部类的bias。同时提升了头部和尾部的性能。 思路 目前存…

easyrecovery2023电脑文件数据恢复软件功能介绍

EasyRecovery功能全面&#xff0c;即便是没有经验的小白用户也可以很快上手&#xff0c;让你足不出户即可搞定常见的数据丢失问题。 在使用和操作存储设备期间&#xff0c;数据丢失问题在所难免。比如&#xff0c;误删除某个文件、不小心将有数据的分区格式化、误清空了有重要…

2023“认证杯”数学中国数学建模赛题浅析

2023年认证杯”数学中国数学建模如期开赛&#xff0c;本次比赛与妈杯&#xff0c;泰迪杯时间有点冲突。因此&#xff0c;个人精力有限&#xff0c;有些不可避免地错误欢迎大家指出。为了大家更方便的选题&#xff0c;我将为大家对四道题目进行简要的解析&#xff0c;以方便大家…

【vue3】04-vue基础语法补充及阶段案例

文章目录vue基础语法补充vue的computedvue的watch侦听书籍购物车案例vue基础语法补充 vue的computed computed&#xff1a;用于声明要在组件实例上暴露的计算属性。&#xff08;官方文档描述&#xff09; 我们已经知道&#xff0c;在模板中可以直接通过插值语法显示一些data中…

智能网卡相关知识(smart nic 、DPU)

网卡作为穿行在网络与计算之间的桥梁&#xff0c;是可以解决计算瓶颈的关键硬件。 随着CPU 密度和数据中心网络带宽的进一步提升&#xff0c;用户对预期性能的需求&#xff0c;系统运行平稳性都会有更高的要求。云厂商一方面面临巨大的成本压力&#xff0c;另一方面面临巨大的…

新一代异步IO框架 io_uring | 得物技术

1.Linux IO 模型分类 相比于kernel bypass 模式需要结合具体的硬件支撑来讲&#xff0c;native IO是日常工作中接触到比较多的一种&#xff0c;其中同步IO在较长一段时间内被广泛使用&#xff0c;通常我们接触到的IO操作主要分为网络IO和存储IO。在大流量高并发的今天&#xff…

光伏电池片技术N型迭代,机器视觉检测赋能完成产量“弯道超车”

电池片是光伏发电的核心部件&#xff0c;其技术路线和工艺水平直接影响光伏组件的发电效率和使用寿命。随着硅料、硅片技术逐渐接近其升级迭代空间的瓶颈&#xff0c;电池片环节正处于技术变革期&#xff0c;是光伏产业链中迭代最快的部分。P型中PERC电池片是现阶段市场的主流产…

C/C++每日一练(20230413)

目录 1. 与浮点数A最接近的分数B/C &#x1f31f; 2. 比较版本号 &#x1f31f;&#x1f31f; 3. 无重复字符的最长子串 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每…

Multi-modal Alignment using Representation Codebook

Multi-modal Alignment using Representation Codebook 题目Multi-modal Alignment using Representation Codebook译题使用表示代码集的多模态对齐期刊/会议CVPR 摘要&#xff1a;对齐来自不同模态的信号是视觉语言表征学习&#xff08;representation learning&#xff09;的…

Vue2_02_指令

模板语法 — Vue.js (vuejs.org) 指令 (Directives) 是带有 v- 前缀的特殊 attribute 参数 一些指令能够接收一个“参数”&#xff0c;在指令名称之后以冒号表示 <a v-bind:href"url">...</a> 动态参数 可以用方括号括起来的 JavaScript 表达式作为一…

JWT与Token详解

前言&#xff1a;JWT全称“JSON Web Token”&#xff0c;是实现Token的机制。官网&#xff1a;https://jwt.io/ JWT的应用 JWT用于登录身份验证。用户登录成功后&#xff0c;后端通过JWT机制生成一个token&#xff0c;返回给客户端。客户端后续的每次请求都需要携带token&…

常用加密算法

目录 常见的加密算法可以分成三种&#xff1a; 对称加密算法 DES 3DES AES 非对称加密 RSA ECC Hash算法 MD5 SHA1 算法对比 算法选择 常见的加密算法可以分成三种&#xff1a; 对称加密算法&#xff1b;非对称加密算法&#xff1b;Hash算法&#xff1b;接下来我们…

面试手撕堆排序

堆排序代码如下&#xff08;注释见下&#xff09;&#xff1a; 首先将待排序的数组构造成一个大根堆&#xff0c;此时&#xff0c;整个数组的最大值就是堆结构的堆顶 将堆顶的数与末尾的数交换&#xff0c;此时&#xff0c;末尾的数为最大值&#xff0c;剩余待排序数组个数为n…