100行代码搭建一个IO泄露监测框架

news2025/4/9 1:21:34

大家好,最近由于项目原因,对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会自动帮助我们完成这个操作:
image.png
常见的InputStream、OutputStream 、Scanner 、PrintWriter都实现了AutoCloseable接口,所以文件读写时可以非常方便的使用上面的语法糖。

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

kotlin针对Closeable(实现了AutoCloseable)接口提供了下面的扩展:
image.png
我们常见的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;
    }

    @Override
    protected 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

image.png
由于**CloseGuard**的源码无法直接在AS中查看,这里我们借助http://aospxref.com/android-12.0.0_r3/xref/libcore/dalvik/src/main/java/dalvik/system/CloseGuard.java网站查看下该类的源码:
image.png
CloseGuard.get()方法就是创建了一个CloseGuard对象。

2. 打开文件流

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

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

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

3. 关闭文件流

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

  • 调用CloseGuardclose()方法:

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

  • 关闭文件流;

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

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

  • 调用CloseGuardwarnIfOpen()方法:

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

  • 兜底关闭流;

通过上面的分析可以得知,一旦发生io泄漏,就会通过**reporter.report()**上报,这就是我们监控应用整体io泄漏的关键。
看下reporter是个啥:
image.png
image.png
**reporter**是一个静态变量,本质上是一个实现了**Reporter**接口的默认实现类**DefaultReporter**,默认通过**report()**方法打印io泄漏的系统日志。
同时外部可以注入自定义的实现了**Reporter**接口的类:
image.png
讲到这里大家是不是明白了,如果实现应用层的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大佬的文章:另一种绕过 Android P以上非公开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()
}

如果想要了解SDK无侵入初始化并获取Application,可以参考之前写的一篇文章:SDK无侵入初始化并获取Application。

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

    companion object {
        const val TAG = "IOLeakCanary"

        lateinit var APPLICATION: Context
    }

    /**
     * CloseGuard原始的Reporter接口实现类DefaultReporter
     */
    private var mOriginalReporter: Any? = null

    fun init(application: Context) {
        APPLICATION = application
        val 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接口实现类DefaultReporter
            val 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 false
            methodSetReporter.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 null
                val 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相关详细,并且故意不释放文件流:
image.png
运行下项目,查看logcat日志输出:

image.png

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

六. 总结

其实,如果了解过matrix-io-canary源码的人,应该很快就可以发现,**这不就是matrix-io-canary中io泄漏监测的实现源码吗!**笔者只是在通读了matrix-io-canary之后,通过整理涉及到的相关知识点,以一种更加通俗的方式进行了讲解,希望本篇文章能对你有所帮助。
不过请注意,以上CloseGuard是基于Android12的源码进行的分析,不同的系统版本比如Android8实现是不同的;而且涉及到系统非公开api的访问也是借助了FreeReflection进行了实现,本身Android官方是禁止使用这些非公开api的,所以为了应用的稳定性,建议大家只在debug环境下使用上述逻辑。

七. 参考链接

另一种绕过 Android P以上非公开API限制的办法
matrix-io-canary
Java必须懂的try-with-resources
CloseGuard
SDK无侵入初始化并获取Application

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

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

相关文章

locust 快速入门--一次接口压测

背景&#xff1a; 使用locust&#xff0c;借助webUI&#xff0c;完成一次接口压测 实现步骤&#xff1a; 完成locust环境配置 准备一个locustfile&#xff08;current_limiting_test.py&#xff09; from locust import HttpUser, task, events from locust.env import Envi…

(Python + Selenium4)Web自动化测试自学Day1

目录 文章声明⭐⭐⭐让我们开始今天的学习吧&#xff01;自动打开Chrome浏览器实现自动搜索元素定位常用的元素定位方式By.IDBy.CLASS_NAMEBy.TAG_NAMEBy.NAMEBy.LINK_TEXTBy.PARTIAL_LINK_TEXTBy.CSS_SELECTOR根据id定位根据class定位根据属性定位组合定位 By.XPATH 文章声明⭐…

Spring中的工厂类ApplicationContext和BeanFactory

1.ApplicationContext ApplicationContext的实现类&#xff0c;如下图 ClassPathXmlApplicationContext&#xff1a;加载类路径下 Spring 的配置文件 FileSystemXmlApplicationContext&#xff1a;加载本地磁盘下 Spring 的配置文件 ApplicationContext由BeanFactory派生而…

文件归类妙招:用关键字替换改扩展名方法,文件重命名技巧

在日常工作中&#xff0c;文件的数量会随着时间的推移不断增加。如果文件没有得到适当的归类和整理&#xff0c;就会导致很难找到所需的文件。所以文件归类是非常重要的任务。现在来看云炫文件管理器一些实用的文件归类妙招&#xff1a;用关键字替换修改文件扩展名的方法&#…

【计算机毕业设计】SSM在线化妆品网站

项目介绍 本项目为前后台项目&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员登录,分类管理,产品管理,用户管理,订单管理等功能。 用户角色包含以下功能&#xff1a; 提交订单,用户登录,用户首页,查看…

程序性能优化全能手册

本文聊一个程序员都会关注的问题&#xff1a;性能。 当大家谈到“性能”时&#xff0c;你首先想到的会是什么&#xff1f; 是每次请求需要多长时间才能返回&#xff1f; 是每秒钟能够处理多少次请求&#xff1f; 还是程序的CPU和内存使用率高不高&#xff1f; 这些问题基本上…

3d全景怎么拍摄?应用领域有哪些?

3d全景技术是综合了VR技术和全景拍摄的一种新型应用技术&#xff0c;通过3D全景技术可以为用户带来720度无死角的观看方式和真实的观看体验&#xff0c;那么3d全景是怎么拍摄制作的呢&#xff1f;应用领域又有哪些呢&#xff1f; 3d全景拍摄制作流程其实不难&#xff0c;常见的…

基于日照时数计算逐日太阳辐射

基于日照时数计算逐日太阳辐射

ffmpeg.c(4.3.1)源码剖析

文章目录 前言一、FFmpeg 源码结构图二、ffmpeg.h 头文件详解三、main 函数主要流程分析四、ffmpeg_parse_options1、命令行例子①、解析命令行 split_commandline()②、parse_optgroup()③、MATCH_PER_XXX_OPT() 2、vf 选项解析①、filters②、vf 术语③、avfilter_graph_pars…

分布式协调系统

分布式协调系统 分布式协调系统解决的进程间的通信和协作&#xff0c;根据是否在同一时间和是否相互引用分为四个模型。 示例系统Chubby 主功能&#xff1a;让客户端实现同步&#xff0c;方法是加锁服务 介绍一下系统&#xff1a; 系统由五台服务器构成&#xff0c;通过pax…

Java:Lambda表达式、方法引用

文章目录 1、Lambda表达式1.1 Lambda表达式体验1.2 Lambda表达式的省略形式1.3 Lambda表达式练习 2、方法引用体验3、方法引用符4、引用静态方法5、引用对象的实例方法6、引用类的实例方法7、引用构造方法8、引用数组的构造方法9、方法引用练习9.1 练习19.2 练习29.3 练习3 10、…

CAN通信的基本原理与实现方法

一. CAN协议概念 1.1 CAN 协议简介 CAN 是控制器局域网络 (Controller Area Network) 的简称&#xff0c;它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的&#xff0c;并最终成为国际标准(ISO11519以及ISO11898),是国际上应用最广泛的现场总线之一。差异点如下&…

​三子棋(c语言)

前言&#xff1a; 三子棋是一种民间传统游戏&#xff0c;又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏规则是双方对战&#xff0c;双方依次在9宫格棋盘上摆放棋子&#xff0c;率先将自己的三个棋子走成一条线就视为胜利。但因棋盘太小&#xff0c;三子棋在很多时候会出现和…

【CentOS 7.9】安装搜狗输入法教程

总览 1.如何在 centos7.x 中使用 搜狗输入法 一、安装依赖 1.切换至 root 用户 su root2.更新 yum yum update3.卸载 ibus rpm -e --nodeps ibus4.安装 epel 源 yum -y install epel-release5.安装 fcitx 环境、qtwebkit包 和 alien 转换工具等&#xff08;可以将 .deb …

静态网页设计——旅游景点介绍(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a; https://www.bilibili.com/video/BV1f64y1N7uH/?vd_source5f425e0074a7f92921f53ab87712357b 使用技术&#xff1a;HTMLCSSJS&#xff08;…

系列十一、(三)Sentinel控制台

一、Sentinel控制台 二、实时监控 2.1、概述 实时监控&#xff0c;顾名思义是用来实时监控的&#xff0c;具体监控的是接口请求通过的QPS和拒绝的QPS&#xff0c;默认情况下没有访问记录&#xff0c;所以看不到任何记录&#xff0c;需要访问接口才会有记录。另外需要注意&…

[4K80 AI ISP IPC芯片]

4K80 AI ISP IPC芯片 Hi3403V100是一颗面向监控市场推出的专业 Ultra-HD Smart IP Camera SOC&#xff0c;该芯片最高支持四路sensor输入&#xff0c;支持最高4K60的ISP图像处理能力&#xff0c;支持3F WDR加粗样式、多级降噪、六轴防抖、硬件拼接等多种图像增强和处理算法&am…

easyrecovery16 (硬盘数据恢复软件)免费版

EasyRecovery是由球著名数据厂商Kroll Ontrack出品的一款便捷实用&#xff0c;功能强大的硬盘数据恢复软件。它能够全面恢复删除丢失数据&#xff0c;支持包括文档、表格、图片、音视频等各种文件类型。支持恢复不同存储介质数据&#xff1a;硬盘、光盘、U盘/移动硬盘、数码相机…

Vue3-35-路由-路由守卫的简单认识

什么是路由守卫 路由守卫&#xff0c;就是在 路由跳转 的过程中&#xff0c; 可以进行一些拦截&#xff0c;做一些逻辑判断&#xff0c; 控制该路由是否可以正常跳转的函数。常用的路由守卫有三个 &#xff1a; beforeEach() : 前置守卫&#xff0c;在路由 跳转前 就会被拦截&…

C++学习笔记——类作用域和抽象数据类型

目录 一、C类作用域 类内作用域 类外作用域 二、类作用域案列详细的解释说明 三、抽象数据类型 四、总结 类作用域 抽象数据类型&#xff08;ADT&#xff09; 五、图书馆管理系统 一、C类作用域 在C中&#xff0c;类作用域是指类定义中声明的标识符&#xff08;成员变…