美文网首页
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