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()
        }
    }
}
相关推荐
爱笑鱼1 小时前
Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?
android
Android-Flutter2 小时前
Kotlin 冷流与热流详解
android·kotlin
zhangphil2 小时前
Android ContentProvider/ContentResolver读图片,跨进程 IPC慢
android
yueqc12 小时前
Android .so 文件压缩
android·so·包体积
龚礼鹏3 小时前
深入解析 Android Automotive (AAOS) 启动流程与 CarService 核心机制(基于 Android 16 最新源码视角)
android
rosmis3 小时前
agent各指标定义
android·java·开发语言
风样滴男人哟4 小时前
PHP特性之反射类ReflectionClass机制
android·开发语言·php
小孔龙5 小时前
RenderNode 与 DisplayList:Android 硬件加速的绘制记录与复用
android·性能优化
qizayaoshuap5 小时前
鸿蒙 ArkTS 实战:搜索列表 城市实时过滤(示例 94)
android·华为·harmonyos
朝星6 小时前
Framework筑基之Handler消息机制5(应用层)
android