前言
大家写代码时经常会忘记释放 Closeable
资源,在大多数情况下,这不会导致任何严重问题。但有时候在垃圾回收器回收先前实例之前,当我们需要该资源时,它可能会导致资源不可用的棘手错误。
Java 时代大家会使用 try/finally
做资源释放, Kotlin 则推荐大家使用 use
,但大家在使用时会存在使用不正确的现象。
本文带大家梳理各种 Closeable
资源回收的方法和注意事项。
1. try/finally
kt
val file = File("paht")
var inputStream: InputStream? = null
try {
inputStream = file.inputStream()
// use inputStream
inputStream = null
} finally {
inputStream?.close()
}
如上,使用 try/finally
代码块时,我们不得不在代码块外部对资源进行可变引用,以便在两个代码块中都能访问它。由于 inputStream
是个 var
且是个 Nullable
, 使用起来有诸多不便。
因此,很多人会改为这样:
kt
val inputStream = file.inputStream()
// ...
try {
// 使用 inputStream
} finally {
inputStream.close()
}
这也很危险,一旦以为某些逻辑造成 finally
没有执行,就会造成 inputStream
的内存泄漏
举个例子
kt
val (fileA, fileB) = files()
val outputStream = fileA.outputStream()
val inputStream = fileB.inputStream() // 会抛出IO异常
try {
// 使用流
} finally {
inputStream.close()
outputStream.close()
}
上面代码很可能在 fileB.inputStream()
抛出异常,当前函数就结束执行了,此时 outputStream
就泄露了。
这毕竟是异常情况,但正常情况下也会遭遇风险,影响面远比异常情况大得多
kt
var inputStream: InputStream? = null
try {
inputStream = file.inputStream()
// 使用流
} finally {
// 什么也不做
}
我们可能会简单地忘记关闭资源,或者在重构时意外删除 .close ()
调用。这段代码仍然可以编译,会使资源保持未关闭状态。
当你自认为不会犯这种低级错误时,往往 Bug 就理你不远了。要知道一个实际项目的代码往往复杂得多,只缘身在此山中。
每次实例化一个 Closeable
资源时,我们都必须记住在某个时刻正确关闭它,这无疑增加了极大地心智负担。
2. use{...}
use ()
函数是朝着正确方向迈出的一步。与 try/finally
代码块相比,它显著改善了这种情况。
kt
file.inputStream().use { stream ->
// 使用流
}
这种结构更简洁,同时也更安全。它还清晰地界定了流的有效作用域。以前,即使资源已关闭,对它的引用仍然存在。虽然可以将其重写为 null,但变量仍然存在。
最重要的是,我们不再会忘记关闭资源!
但是,我见过很多代码仍然会写成下面这样:
kt
val inputStream = file.inputStream()
if (/*条件*/) {
inputStream.use { stream ->
// 使用流
}
}
inputStream
的初始实例化游离于 use
之外,增加了无法回收的风险。所以 use ()
函数虽然是可关闭资源管理方面的积极进展,但它只解决了部分问题。
3. 自定义作用域函数
use ()
函数确实能保证可关闭资源正常关闭,但它没解决资源初始化的问题,也没法保证资源肯定是在 use ()
代码块里用的。这就得开发者自己注意这些关键步骤,就很容易出岔子,导致资源泄漏。
每个 Closeable
资源的手动实例化都可能导致资源泄漏。我们可以封装自定义作用域函数来减少错误写法的可能。如下
kt
val file = File("path")
file.withInputStream {
// this: inputStream
}
作用域函数 的概念大家应该知道了(例如 let, also, run, apply
等),简单说定义一个参数为 lambda 的函数,提供一个面相某context 的作用域
作用域函数定义如下:
kt
inline fun <T> File.withInputStream(block: InputStream.() -> T): T = inputStream().use(block)
当然,作用域函数的定义方式是多样的:
kt
file.withInputStream { // this: InputStream
// ...
}
file.useInputStream { stream ->
// ...
}
withInputStream(file) { // this: InputStream
// ...
}
withInputStream(file) { stream ->
// ...
}
以上这些写法理论上都可行。大家按照个人喜好定义节课。这里仅提供一些建议:
决定用扩展函数还是普通函数的时候,可以看 Closeable
是如何创建的,如果是通过目标对象上的方法创建时,推荐使用扩展函数,如果需要借助其他外部函数创建,比较适合定义成普通函数。
kt
// 通过在 file 上调用函数实例化:file.inputStream()
file.withInputStream {
// ...
}
// 通过调用构造函数实例化:ZipFile(file)
withInputStream(file) {
// ...
}
如果定义为扩展函数,建议在 lambda 中将创建的 Closable
作为 this
传参,而非 it
。 在 lambda 内,二次调用扩展函数会更加顺手:
kt
file.withInputStream { // this: InputStream
// 作为一个 receiver type 使用
withInputStreamReader {
...
// 作为 parameter 使用
withInputStreamReader(this) {
...
4. 作用域函数 + Flow:长生命周期资源管理
前面说的都是拿到资源、用完就关的情况,但要是我们得长时间开着资源呢?
作用域函数搭配协程就能搞定。
kt
fun lazyContent(fixedReader: Reader, bufferSize: Int): Flow<CharArray> = flow {
var buffer = CharArray(bufferSize)
var bytesRead = fixedReader.read(buffer)
while (bytesRead != -1) {
if (bytesRead == bufferSize) {
emit(buffer.clone())
} else {
emit(buffer.copyOf(bytesRead))
}
bytesRead = fixedReader.read(buffer)
}
}
我发现对于"订阅式"的资源,大家对资源释放的观念反而更强,所以就不多给什么建议了。
5. use + try/catch:兜底异常捕捉
翻看源码可以知道,use
内部就是通过 try/finally
实现资源回收的。所以我们还需要在 use
之外使用 try/finally
吗?
实际上是需要的,use
内部使用 try/finally
只是为了做资源释放,实际上 Exception
是会继续抛出的,所以为了捕捉 use
本身抛出的异常,很多场景下仍然需要使用 try
叠加 use
kt
fun main() {
try {
FileInputStream("test.txt").use { inputStream ->
// 使用 inputStream 进行操作
val data = inputStream.read()
println("Read data: $data")
}
} catch (e: IOException) {
e.printStackTrace()
}
}
比如上面例子,在创建 FileInputStream
对象时可能因为文件不存在发生异常,亦或者 use {...}
内部抛出各种异常,都需要 try
来兜底,只是我们不需要加 finally
做资源回收了。
此外,当需要处理多个资源时,有时不得不在 use
之前提前实例化 Stream
对象,此时可先使用 use
函数处理单个资源,再用 try/finally
块确保多个资源都能被正确关闭。
kt
fun main() {
var inputStream: FileInputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = FileInputStream("input.txt")
outputStream = FileOutputStream("output.txt")
inputStream.use { inStream ->
outputStream.use { outStream ->
// 从输入流读取数据并写入输出流
var byte: Int
while (inStream.read().also { byte = it } != -1) {
outStream.write(byte)
}
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
try {
inputStream?.close()
} catch (e: IOException) {
e.printStackTrace()
}
try {
outputStream?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}