Anroid Kotlin:如何正确回收 Closeable 资源

前言

大家写代码时经常会忘记释放 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()
        }
    }
}
相关推荐
w23617346011 小时前
Android四大核心组件
android·四大组件
Dnelic-1 小时前
移动通信行业术语
android·telephony·自学笔记
每次的天空2 小时前
Android学习总结之扩展基础篇(一)
android·java·学习
心之所向,自强不息3 小时前
关于Android Studio的Gradle各项配置2
android·gradle·android studio
EQ-雪梨蛋花汤3 小时前
【Flutter】Unity 三端封装方案:Android / iOS / Web
android·flutter·unity
foenix663 小时前
PicoVR眼镜在XR融合现实显示模式下无法显示粒子问题
android·unity·c#·xr·pico
一杯凉白开4 小时前
为了方便测试,程序每次崩溃的时候,我都让他跳转新页面,把日志显示出来
android
小馬佩德罗5 小时前
Android 系统的兼容性测试 - CTS
android·cts
缘来的精彩6 小时前
Android ARouter的详细使用指南
android·java·arouter
风起云涌~6 小时前
【Android】ListView控件在进入|退出小窗下的异常
android