咱们好,最近因为项目原因,对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会主动协助咱们完结这个操作:

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

常见的InputStream、OutputStream 、Scanner 、PrintWriter都完结了AutoCloseable接口,所以文件读写时能够十分方便的运用上面的语法糖。

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

kotlin针对Closeable(完结了AutoCloseable)接口供给了下面的扩展:

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

咱们常见的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

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

因为CloseGuard的源码无法直接在AS中检查,这儿咱们凭借aospxref.com/android-12.…网站检查下该类的源码:

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

CloseGuard.get()方法便是创立了一个CloseGuard目标。

2. 翻开文件流

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

FileInputStream构造方法首要干了两件工作:

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

  • 一起还会调用CloseGuard.open()方法:

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

这个方法首要干的工作便是创立了一个Throwable目标,获取当时流创立的仓库,并赋值给CloseGuardcloserNameOrAllocationInfo字段。


3. 封闭文件流

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

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

  • 调用CloseGuardclose()方法:

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

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

  • 封闭文件流;

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

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

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

  • 调用CloseGuardwarnIfOpen()方法:

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

    假如closerNameOrAllocationInfo字段不为空,阐明FileInputStreamclose() 封闭文件流的方法漏了调用,产生了io走漏,调用reporter.report() 方法并传入closerNameOrAllocationInfo参数(这个参数上面有说:保存了流创立时的仓库,一旦获取到咱们就能很快知道哪个当地创立的流产生了走漏)。

  • 兜底封闭流;

经过上面的剖析能够得知,一旦产生io走漏,就会经过reporter.report() 上报,这便是咱们监控运用整体io走漏的关键。

看下reporter是个啥:

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

reporter是一个静态变量,本质上是一个完结了Reporter接口的默认完结类DefaultReporter ,默认经过report() 方法打印io走漏的体系日志。

一起外部能够注入自定义的完结了Reporter接口的类:

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

讲到这儿咱们是不是理解了,假如完结运用层的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相关具体,而且故意不释放文件流:

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

运转下项目,检查logcat日志输出:

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

能够看到有告警日志打印,并经过日志直接就定位到了反常逻辑:代码第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


本文正在参加「金石方案」