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条目:
- 更新统计数据(条目计数、大小累加)
- 执行路径安全校验,防止恶意Zip的路径遍历攻击
- 执行解压操作(创建目录/分块写入文件)
- 实时回调进度信息
优化后的代码示例
// 自定义进度回调接口,按需调整参数 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
相关产品推荐
相关产品推荐

