You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin中高效获取ZipInputStream内条目数量的方法

高效处理ZipInputStream:一次遍历完成统计与解压

问题背景

仅能获取Zip文件的InputStream,当前需要两次读取ZipInputStream:第一次统计条目数量(及总大小),第二次执行解压。两次遍历导致大Zip文件处理速度缓慢,尤其在Android平台上统计阶段耗时过长,希望实现一次遍历同时完成统计与解压,进而展示解压进度。

当前两次遍历的低效模式(逻辑示例):

// 第一次遍历:仅统计元数据
ZipInputStream(inputStream).use { zis ->
    var count = 0L
    var totalSize = 0L
    generateSequence { zis.nextEntry }.forEach { entry ->
        count++
        totalSize += entry.size
        zis.skip(entry.size) // 跳过内容读取,仅统计
    }
}
// 第二次遍历:执行解压
// ... 重复解压逻辑代码

优化方案:一次遍历完成统计+解压

核心思路是在同一次遍历Zip条目的过程中,同时完成统计信息累加与文件解压操作,避免重复读取流。既可以实时更新进度(已处理条目数、已解压大小),又无需额外的流重置或二次读取。

实现步骤

  • 初始化统计变量:已处理条目数、已解压大小,可选预统计总条目数/总大小(视流是否可重置而定)
  • 遍历每个Zip条目:
    1. 更新统计数据(条目计数、大小累加)
    2. 执行路径安全校验,防止恶意Zip的路径遍历攻击
    3. 执行解压操作(创建目录/分块写入文件)
    4. 实时回调进度信息

优化后的代码示例

// 自定义进度回调接口,按需调整参数
interface UnzipProgressCallback {
    fun onProgress(currentBytes: Long, totalBytes: Long, currentEntryName: String)
}

fun unzipWithProgress(
    inputStream: InputStream,
    outputDirectory: File,
    progressCallback: UnzipProgressCallback
) {
    val outputDirStr = outputDirectory.absolutePath

    if (inputStream.markSupported()) {
        // 流支持重置时,先预统计总条目数与总大小,再解压
        inputStream.mark(Int.MAX_VALUE)
        var totalEntries = 0L
        var totalSize = 0L

        ZipInputStream(inputStream).use { tempZis ->
            generateSequence { tempZis.nextEntry }.forEach { entry ->
                totalEntries++
                totalSize += entry.size
                tempZis.skip(entry.size)
            }
        }

        inputStream.reset()
        ZipInputStream(inputStream).use { zis ->
            processEntries(zis, outputDirectory, outputDirStr, totalSize, progressCallback)
        }
    } else {
        // 流不可重置时,直接边解压边统计,实时更新进度
        ZipInputStream(inputStream).use { zis ->
            processEntries(zis, outputDirectory, outputDirStr, null, progressCallback)
        }
    }
}

private fun processEntries(
    zipInputStream: ZipInputStream,
    outputDirectory: File,
    outputDirStr: String,
    totalSize: Long?,
    progressCallback: UnzipProgressCallback
) {
    var unzippedSize = 0L
    var currentTotalSize = totalSize ?: 0L

    generateSequence { zipInputStream.nextEntry }.forEach { entry ->
        // 安全校验:禁止写入输出目录外的文件
        val file = File(outputDirectory, entry.name)
        val canonicalPath = file.canonicalPath
        val path = file.path
        if (!canonicalPath.startsWith(outputDirStr) && !path.startsWith(outputDirStr)) {
            throw SecurityException("危险的Zip条目: ${entry.name}")
        }

        if (entry.isDirectory) {
            file.mkdirs()
            // 目录无内容,直接回调进度
            progressCallback.onProgress(unzippedSize, currentTotalSize, entry.name)
        } else {
            file.parentFile?.mkdirs()
            // 分块写入,实时更新已解压大小
            val buffer = ByteArray(8192)
            file.outputStream().use { outputStream ->
                var bytesRead: Int
                while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
                    outputStream.write(buffer, 0, bytesRead)
                    unzippedSize += bytesRead
                    // 若未预统计总大小,实时计算当前总大小
                    if (totalSize == null) {
                        currentTotalSize = unzippedSize + (entry.size - bytesRead)
                    }
                    progressCallback.onProgress(unzippedSize, currentTotalSize, entry.name)
                }
            }
        }
    }
}

关键说明

  • 流可重置场景:若InputStream支持mark/reset(如ByteArrayInputStream),可先预统计总大小与条目数,让进度条展示完整的全局进度。
  • 流不可重置场景:直接边解压边统计,进度条可展示“已处理xx字节/动态计算总大小”的形式,避免二次读取流的额外开销。
  • 分块写入优化:使用固定缓冲区分块写入文件,每次写入后更新进度,避免大文件解压时进度长时间无响应。
  • 安全校验保留:必须保留路径校验逻辑,防止恶意Zip文件写入到输出目录外的敏感位置。

内容的提问来源于stack exchange,提问作者johngray1965

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 15:50:29