美文网首页
StrictMode:Explicit termination

StrictMode:Explicit termination

作者: 未子涵 | 来源:发表于2023-07-11 14:57 被阅读0次

    背景

    明明代码中已经对 IO操作做了完善的关闭处理,在 Android StrictMode 下仍然收到了“IO泄漏”的告警?这篇文章就来分析一下这个诡异的问题。

    问题

    在 Android 严格模式(StrictMode)下,检查代码安全性,出现IO泄漏的告警,但是检查代码,是已经确保了在 finaly 中对io做close的。

    • 机型:android 9
    • 说明:只测试了华为系设备(荣耀、华为),其他厂商未确认,并且华为系其他版本无法复现
    • 错误堆栈:
    java.lang.Throwable: Explicit termination method 'end' not called
    at dalvik.system.CloseGuard.open(CloseGuard.java:221)
    at java.util.zip.Inflater.<init>(Inflater.java:114)
    at java.util.zip.GZIPInputStream.<init>(GZIPInputStream.java:77)
    at java.util.zip.GZIPInputStream.<init>(GZIPInputStream.java:92)
    

    分析

    本次报的问题,与之前不一样,之前错误信息是:

    Explicit termination method 'close' not called
    

    上面这个信息是指没有对使用的 io 流做 close 处理,或者处理的不够严谨,标准处理一般如下:

        public static void test(File f) {
                if (f != null && f.exists()) {
                    FileInputStream fis = null;
                    try {
                        fis = new FileInputStream(f);
                        // 对fis的读取操作
                        // ...
                    } catch (Throwable t) {
                        t.printStackTrace();
                    } finally {
                        if (fis != null) {
                            try {
                                fis.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
        }
    

    但本次的错误信息是:

    Explicit termination method 'end' not called
    

    一个是 close 方法未调用,一个是 end 方法未调用。查看 CloseGuard.java,找到此错误信息出处:

    public void openWithCallSite(String closer, String callsite) {
        // always perform the check for valid API usage...
        if (closer == null) {
            throw new NullPointerException("closer == null");
        }
        // ...but avoid allocating an allocation stack if "disabled"
        if (!stackAndTrackingEnabled) {
            closerNameOrAllocationInfo = closer;
            return;
        }
        // Always record stack trace when tracker installed, which only happens in tests. Otherwise, skip expensive
        // stack trace creation when explicit callsite is passed in for better performance.
        Tracker tracker = currentTracker;
        if (callsite == null || tracker != null) {
            String message = "Explicit termination method '" + closer + "' not called";
            Throwable stack = new Throwable(message);
            closerNameOrAllocationInfo = stack;
            if (tracker != null) {
                tracker.open(stack);
            }
        } else {
            closerNameOrAllocationInfo = callsite;
        }
    }
    
    /**
     * {@code open} initializes the instance with a warning that the caller
     * should have explicitly called the {@code closer} method instead of
     * relying on finalization.
     *
     * @param closer non-null name of explicit termination method. Printed by warnIfOpen.
     * @throws NullPointerException if closer is null.
     */
    public void open(String closer) {
        openWithCallSite(closer, null /* callsite */);
    }
    

    可见,入参 closer 是关键,最初是由 open(String closer) 方法传入的,此方法的注释是说:

    open方法初始化一个实例,并警告调用者应该显式地调用参数 closer 指定的方法,而不是依赖finalization。
    @param closer 显式调用的终止方法名(非空),由 warnIfOpen 方法打印
    

    也就是说,如果不显示调用 closer 参数指定的方法来终止该对象,就会在 logcat 中打印警告。我们遇到日志显示的是 "method 'end' not called" ,所以就是 end() 方法。

    IO流使用完成后要 close ,这是我们已经做过的优化:

        public static void test(File f) {
                if (f != null && f.exists()) {
                    FileInputStream fis = null;
                    GZIPInputStream gzis = null;
                    ObjectInputStream ois = null;
                    try {
                        fis = new FileInputStream(f);
                        gzis = new GZIPInputStream(fis);
                        ois = new ObjectInputStream(gzis);
                        Object object = ois.readObject();
                    } catch (Throwable t) {
                        t.printStackTrace();
                    } finally {
                        closeIOs(ois, gzis, fis);
                    }
                }
        }
    
        public static void closeIOs(Closeable... ios) {
            if (ios != null && ios.length > 0) {
                for (Closeable item : ios) {
                    try {
                        if (item != null) {
                            item.close();
                        }
                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                }
            }
        }
    

    从代码上看,已经没有任何未 close 的漏洞了,但却还是报了类似的警告,所以唯一的切入点就是 end 方法。查看各个 InputStream 派生类,都未发现有公开的 end 方法,回头看警告信息:

    at java.util.zip.Inflater.<init>(Inflater.java:114)
    at java.util.zip.GZIPInputStream.<init>(GZIPInputStream.java:77)
    

    是在 GZIPInputStream 的构造过程中发生的,因此可以从其构造方法切入:

        public GZIPInputStream(InputStream in, int size) throws IOException {
            super(in, new Inflater(true), size);
            // Android-removed: Unconditionally close external inflaters (b/26462400)
            // usesDefaultInflater = true;
            // BEGIN Android-changed: Do not rely on finalization to inf.end().
            // readHeader(in);
            try {
                readHeader(in);
            } catch (Exception e) {
                inf.end();
                throw e;
            }
            // END Android-changed: Do not rely on finalization to inf.end().
        }
    

    这里发现一个 Inflater.end() 方法,再结合代码注释:

    // BEGIN Android-changed: 不要依赖 finalization 去做inf.end()
    // readHeader(in);
    try {
      readHeader(in);
    } catch (Exception e) {
      inf.end();
      throw e;
    }
    // END Android-changed: 不要依赖 finalization 去做inf.end()
    

    再回头看文初 CloseGuard.java 的源码注释:

    open方法初始化一个实例,并警告调用者应该显式地调用参数 closer 指定的方法,而不是依赖finalization。
    

    很明显,上面的 Android-changed 修改就是为了响应 open 方法中 "不要依赖finalization" 的规定,再来对比 JDK 中的 GZIPInputStream.java 源码:

        public GZIPInputStream(InputStream in, int size) throws IOException {
            super(in, new Inflater(true), size);
            usesDefaultInflater = true;
            readHeader(in);
        }
    

    可见,Android SDK 就是对 readHeader(in); 增加了异常捕获,并主动做了 inf.end()

    至此,基本可以确定,这里的 Android-changed 就是为了解决一个系统bug,这个bug就是:未遵守 “不要依赖 finalization” 的规定。

    接下来,为了进一步验证,我们来看看这个bug是什么时候修复的。

    Android-28:

        public GZIPInputStream(InputStream in, int size) throws IOException {
            super(in, new Inflater(true), size);
            // Android-changed: Unconditionally close external inflaters (b/26462400)
            // usesDefaultInflater = true;
            readHeader(in);
        }
    

    Android-29:

        public GZIPInputStream(InputStream in, int size) throws IOException {
            super(in, new Inflater(true), size);
            // Android-removed: Unconditionally close external inflaters (b/26462400)
            // usesDefaultInflater = true;
            // BEGIN Android-changed: Do not rely on finalization to inf.end().
            // readHeader(in);
            try {
                readHeader(in);
            } catch (Exception e) {
                inf.end();
                throw e;
            }
            // END Android-changed: Do not rely on finalization to inf.end().
        }
    

    可见,这是从 android-29 (Android 10)开始修复的bug

    既然这个 Inflater.end() 是必须主动调用的,但又没有对开发者公开,那必定系统会在某个时机自动调用,查看 GZipInputStream.javaclose() 方法:

        public void close() throws IOException {
            if (!closed) {
                super.close();
                eos = true;
                closed = true;
            }
        }
    

    进入其 super.close() 方法,这里的 super 就是 InflaterInputStream.java

        public void close() throws IOException {
            if (!closed) {
                // Android-changed: Unconditionally close external inflaters (b/26462400)
                //if (usesDefaultInflater)
                inf.end();
                in.close();
                closed = true;
            }
        }
    

    可见,只要调用 close() 方法就会自动调用 inf.end() ,并且确认了 android-28android-29 的代码都如上所示,所以这里没有系统版本上的bug。

    说明,开发者只要保证调用 close() 方法即可,不用关注 inf.end() 的调用,它会由系统自动调用。

    结论

    这是 Android 10 以下版本的系统bug,不需要处理,只要确保 finaly 中调用 close 释放io资源即可。

    相关文章

      网友评论

          本文标题:StrictMode:Explicit termination

          本文链接:https://www.haomeiwen.com/subject/fqtuudtx.html