背景
明明代码中已经对 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.java
的 close()
方法:
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-28
和 android-29
的代码都如上所示,所以这里没有系统版本上的bug。
说明,开发者只要保证调用 close()
方法即可,不用关注 inf.end()
的调用,它会由系统自动调用。
结论
这是 Android 10 以下版本的系统bug,不需要处理,只要确保 finaly 中调用 close 释放io资源即可。
网友评论