手写一个IO泄露监测框架

news2024/10/4 13:33:14

作者:长安皈故里

大家好,最近由于项目原因,对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;
    }

    @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

由于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? = 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相关详细,并且故意不释放文件流:

运行下项目,查看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.coloradmin.cn/o/421592.html

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

相关文章

AEC-Q认证介绍及所有最新工程文件下载

AEC-Q认证介绍及所有最新文件&#xff08;英文版&#xff09;下载 注意&#xff1a; 更多交流及资料请加V&#xff1a;john-130 AEC-Q认证介绍 1&#xff0c;AEC-Q认证总体情况介绍 &#xff08;​1&#xff09;AEC&#xff08;Automotive Electronics Council&#xff09;…

图像分类:Pytorch图像分类之-- MobileNet系列模型

文章目录前言MobileNetV1模型介绍DW&#xff08;Depthwise Convolution&#xff09;卷积PW &#xff08;Pointwise Convolution&#xff09;卷积深度可分离卷积&#xff08;DWPW&#xff09;ReLU6激活函数的介绍MobileNet V1网络结构MobileNet V1程序MobileNetV2模型介绍Invert…

链接、包管理工具、polyrepo、monorepo以及Lerna 工具的使用

nodejs 链接、包管理工具、多包管理以及Lerna 工具的使用jcLee95&#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/129903902 目 录1. 概述…

bjdctf_2020_babyrop2-fmt-leak canary

1,三连 分析:开了canary&#xff0c;先想办法获取canary值。 2&#xff0c;IDA静态分析&#xff0c;查看可以泄露canary的地方&#xff0c;否则只能爆破了 发现可以格式化字符串函数泄露的地方&#xff1a; 栈帧结构&#xff1a; 高地址 -------------- gift_ret栈帧 ------…

【算法宇宙——在故事中学算法】背包dp之01背包问题

唯手熟尔方成艺&#xff0c;唯读书能致卓越。勤学苦练方可成&#xff0c;路漫漫其修远兮&#xff01; 文章目录前言正文故事总结前言 尽管计算机是门严谨的学科&#xff0c;但正因为严谨&#xff0c;所以要有趣味才能看得下去。在笔者的前几篇算法类文章中&#xff0c;都采用了…

智慧公厕系统的应用示例

近几年&#xff0c;在一些高速服务区或者一些城市的公共厕所当中&#xff0c;总会看见一些富有科技感的硬件&#xff0c;比如厕位有无人指示灯、厕所除臭杀菌机、智能取纸机、智能洗手台镜面广告机等。现在在衡量城市发展的过程中&#xff0c;总会以城市的建设&#xff0c;城市…

Weblogic远程代码执行漏洞 CVE-2023-21839

漏洞简介 WebLogic Core远程代码执行漏洞&#xff08;CVE-2023-21839&#xff09;&#xff0c;该漏洞允许未经身份验证的远程攻击者通过T3/IIOP协议进行 JNDI lookup 操作&#xff0c;破坏易受攻击的WebLogic服务器&#xff0c;成功利用此漏洞可能导致Oracle WebLogic服务器被接…

MySQL可重复读事务隔离具体是怎么实现的

事务的启动会有的操作 事务的隔离等级有四种&#xff0c;现在说默认的可重复读&#xff0c;可重复读就是一个事务执行过程中看到的数据&#xff0c;总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下&#xff0c;未提交变更对其他事务也是不可见的。 可重复…

Java阶段一Day22

Java阶段一Day22 文章目录Java阶段一Day22线程安全synchronized教师总结新单词多线程多线程并发安全问题概念例synchronized关键字同步方法同步块在静态方法上使用synchronized互斥锁总结重点:多线程并发安全问题聊天室(续)实现服务端发送消息给客户端服务端转发消息给所有客户…

内网穿透实现在外远程连接RabbitMQ服务

文章目录前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址转载自远控源码文章&#xff1a;无公网IP&#xff0c;…

Linux Systemd type=simple和type=forking的区别

Typeforking 使用Typeforking时&#xff0c;要求ExecStart启动的命令自身就是以daemon模式运行的。 而以daemon模式运行的进程都有一个特性&#xff1a;总是会有一个瞬间退出的中间父进程&#xff0c;例如&#xff0c;nginx命令默认以daemon模式运行&#xff0c;所以可直接将其…

Nodejs vm/vm2沙箱逃逸

文章目录什么是沙箱以及VM&#xff1f;vm模块nodejs作用域vm沙箱vm沙箱逃逸vm2例题分析&#xff1a;&#xff08;待补充&#xff09;[HFCTF2020]JustEscape[HZNUCTF 2023 final]eznode參考文章:什么是沙箱以及VM&#xff1f; 什么是沙箱&#xff1a; 沙箱就是能够像一个集装箱…

Ansys Speos | 联合 optiSLang 背光板设计优化方案

在这个例子中&#xff0c;讲述如何建模一个典型的背光单元及其与亮度和均匀性有关的照度分布。其中一个关键特点是使用了Speos 3D Texture功能&#xff0c;这是最初开发的用于背光单元产品&#xff0c;并可用于设计导光板&#xff0c;亮度增强膜(BEF)和由数千/数百万组成的背光…

《程序员面试金典(第6版)》面试题 10.03. 搜索旋转数组(二分法,分钟思想,入门题目)

题目描述 搜索旋转数组。给定一个排序后的数组&#xff0c;包含n个整数&#xff0c;但这个数组已被旋转过很多次了&#xff0c;次数不详。请编写代码找出数组中的某个元素&#xff0c;假设数组元素原先是按升序排列的。若有多个相同元素&#xff0c;返回索引值最小的一个。 示例…

C学习笔记2

1、二进制由 0 和 1 两个数字组成&#xff0c;使用时必须以0b或0B&#xff08;不区分大小写&#xff09;开头 2、符号位进制形式进制数据 &#xff08;进制形式决定后面的数据是哪种进制&#xff09; 3、合法的二进制 int a 0b101; // 0b是二进制的进制形式 101是进制…

buildroot使用外部编译链编译bluez蓝牙工具

在开发ublox w263 wifi蓝牙时&#xff0c;之前是使用yocto系统集成编译出的bluez工具&#xff0c;减少了自己编译工具软件和依赖库的工作&#xff0c;切换项目使用原生linux系统后&#xff0c;所以的软件需要自己编译&#xff0c;不想编译每个依赖文件和库&#xff0c;所以使用…

Pytorch深度学习笔记(三)线性模型

目录 1.机械学习的过程 2.线性模型 推荐课程&#xff1a;2.线性模型_哔哩哔哩_bilibili 1.机械学习的过程 机械学习的过程&#xff1a; 1.准备数据集DataSet——>2.选择模型Model——>3.训练Training——>4.推理Infering 监督学习&#xff1a;用已知标签的训练样本训…

Spark大数据处理讲课笔记3.1 掌握RDD的创建

文章目录零、本节学习目标一、RDD为何物&#xff08;一&#xff09;RDD概念&#xff08;二&#xff09;RDD示例&#xff08;三&#xff09;RDD主要特征二、做好准备工作&#xff08;一&#xff09;准备文件1、准备本地系统文件2、启动HDFS服务3、上传文件到HDFS&#xff08;二&…

4年软件测试工作经验,跳槽之后面试20余家公司的总结

先说一下自己的个人情况&#xff0c;普通二本计算机专业毕业&#xff0c;懂python&#xff0c;会写脚本&#xff0c;会selenium&#xff0c;会性能&#xff0c;然而离职后到今天都没有收到一份offer&#xff01;一直在待业中&#xff0c;从离职第一天就开始准备简历&#xff0c…

快排的非递归实现

其思想与递归实现快排完全相同&#xff0c;可以先将第一次要排序的右边界和左边界先后入栈&#xff0c;然后判断栈 是否为空&#xff0c;不为空就出栈顶元素&#xff0c;并删除一次&#xff0c;由于栈是先进的后出&#xff0c;所以先出来的应该是左界&#xff0c; 再进行一次…