不管是字节流还是字符流,用完之后不及时关闭的话,都会引起句柄的泄露,内存得不到及时回收。所以一般用她们的时候记得最后及时关闭,这是一种良好的编码规范。但当这个问题出现时咱们咱们检测呢,答案是有的,我们可以利用hook技术把原方法地址入口给替换成我们自己的hook地址,然后在自己的方法里实现计数功能,如果打开计数大于1,则存在泄露,将堆栈信息打印出来,因为open和close方法最后都是在so库里实现的,所以只要找到入口地址将他们替换掉就好了,对此感兴趣的小伙伴可以查看so库的hook方法。

ok,咱们今天要讲的重点是检测FileOutputStream 、FileInputStream忘了关闭,怎么检测它?通过阅读源码发现它的里面有这么一个类

// Android-added: CloseGuard support: Log if the stream is not closed.
    @ReachabilitySensitive
    private final CloseGuard guard = CloseGuard.get();

 看注释很明显它是android添加如果流没有关闭则打印信息的类,那么如果想检测是否有没有关闭的FileOutputStream 、FileInputStream,我们可以利用这个类,看一下FileInputStream构造方法

String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkWrite(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        this.fd = new FileDescriptor();

        // Android-changed: Tracking mechanism for FileDescriptor sharing.
        // fd.attach(this);
        this.isFdOwner = true;

        this.append = append;
        this.path = name;

        // Android-added: BlockGuard support.
        BlockGuard.getThreadPolicy().onWriteToDisk();

        open(name, append);

        // Android-added: CloseGuard support.
        guard.open("close");

最后一行代码调用了 guard.open("close");,好的,进去瞧一瞧

public static CloseGuard get() {
        if (!ENABLED) {
            return NOOP;
        }
        return new CloseGuard();
    }

可以看到CloseGuard获得,ENABLED是静态变量默认为true,FileOutputStream 、FileInputStream都没有主动设置它,所以,每一个FileOutputStream 、FileInputStream都持有不同的CloseGuard引用,而guard.open("close")方法如下

public void open(String closer) {
        // always perform the check for valid API usage...
        if (closer == null) {
            throw new NullPointerException("closer == null");
        }
        // ...but avoid allocating an allocationSite if disabled
        if (this == NOOP || !ENABLED) {
            return;
        }
        String message = "Explicit termination method '" + closer + "' not called";
        allocationSite = new Throwable(message);
    }

这个方法只是声明了一个异常,再来看一下close方法

public void close() {
        allocationSite = null;
    }

只是将open打开的异常变为null,如果你只是打开流而没有关闭流的话allocationSite不为null。

那么什么时候文件句柄泄露触发打印的报告呢,那么这个问题就是垃圾回收器处理的时机了,在垃圾回收器回收对象之前都会调用finalize这个方法,用java的小伙伴都知道我们可以在任何类中复写这个方法做一些自己的清理工作。那么FileOutputStream 、FileInputStream有没有实现这个方法呢?

protected void finalize() throws IOException {
        // Android-added: CloseGuard support.
        if (guard != null) {
            guard.warnIfOpen();
        }

        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {
                // Android-removed: Obsoleted comment about shared FileDescriptor handling.
                close();
            }
        }
    }

重点在第三行代码 guard.warnIfOpen();

 

public void warnIfOpen() {
        if (allocationSite == null || !ENABLED) {
            return;
        }

        String message =
                ("A resource was acquired at attached stack trace but never released. "
                 + "See java.io.Closeable for information on avoiding resource leaks.");

        REPORTER.report(message, allocationSite);
    }

上面分析了,如果主动调用close,allocationSite为null,所以当你忘记关闭文件流的时候就会执行REPORTER.report(message, allocationSite),REPORTER是什么

private static final class DefaultReporter implements Reporter {
        @Override public void report (String message, Throwable allocationSite) {
            System.logW(message, allocationSite);
        }
    }

它默认实现是打印忘记关闭的信息, 那么最终我们应该怎么获取泄露信息自己通知给自己呢,答案就是采用hook技术将REPORTER给替换掉

* Hook for customizing how CloseGuard issues are reported.
     */
    private static volatile Reporter REPORTER = new DefaultReporter();

它是静态的,咱们可以放心的hook了

Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard");
            Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter");
            Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter");
            Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls);
            Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class);

          Object  sOriginalReporter = methodGetReporter.invoke(null);

            methodSetEnabled.invoke(null, true);

            
            MatrixCloseGuard.setEnabled(true);

            ClassLoader classLoader = closeGuardReporterCls.getClassLoader();
            if (classLoader == null) {
                return false;
            }

            methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader,
                new Class<?>[]{closeGuardReporterCls},
                new IOCloseLeakDetector(issueListener, sOriginalReporter)));

hook的常用方式如果是接口大的话就用动态代理,最后当检测到未关闭的文件流的话就会调用到IOCloseLeakDetector中,然后你在里面获取到堆栈信息并打印出来就ok了。