Android应用中使用Zip4j实现分卷加密压缩与存储优化

📅 发布时间:2026/7/6 9:33:57 👁️ 浏览次数:
Android应用中使用Zip4j实现分卷加密压缩与存储优化
1. 为什么你的Android应用需要一个“保险箱”从Zip4j说起你有没有遇到过这样的场景用户在你的App里上传了一堆身份证照片、合同文档或者App自己生成了一份包含用户行为记录的本地日志。这些数据就静静地躺在手机的存储空间里万一手机丢了、被恶意软件扫描了或者用户把手机二手转卖前没清理干净这些敏感信息不就“裸奔”了吗我做过不少涉及用户数据的项目早期也吃过亏觉得文件存到私有目录就万事大吉了直到一次安全审计被揪出风险。所以给这些文件加把“锁”甚至再压缩一下节省空间就成了一个很实际的需求。Android系统自带的ZipOutputStream也能压缩但它有个硬伤不支持加密。这就好比你把贵重物品塞进一个普通行李箱虽然节省了空间但谁都能打开看看。这时候你就需要一个带密码锁的箱子。Zip4j这个库就是专门干这个的。它是个纯Java库所以在Android上用起来毫无障碍核心功能就是创建和解开那些带密码的ZIP压缩包而且支持业界公认强壮的AES加密算法。我实测下来用它来处理几十兆甚至上百兆的用户数据备份、照片批量打包上传既安全又稳当。简单来说如果你需要在App里实现“把一堆文件打包成一个带密码的压缩包”或者反过来“解开一个别人发来的加密压缩包”Zip4j几乎是目前最省心、功能最全的选择。它帮你绕过了复杂的加密算法实现让你能像操作普通文件一样去操作加密压缩文件这对我们开发者来说效率提升不是一点半点。2. 开工前的准备把Zip4j请进你的项目2.1 引入依赖一句话的事现在Android开发基本都用Kotlin了依赖管理也很简单。打开你的app模块下的build.gradle.kts文件如果是Groovy语法文件后缀是.gradle写法略有不同但意思一样在dependencies这个大括号里加上下面这行dependencies { implementation(net.lingala.zip4j:zip4j:2.11.5) // ... 你的其他依赖 }这里的2.11.5是我写这篇文章时的最新稳定版。库的作者维护得挺勤快你可以在GitHub - srikanth-lingala/zip4j上查看最新版本。我一般习惯用稳定版避免踩到新版本的坑。加完依赖同步一下GradleZip4j就准备好为你服务了。2.2 搞定存储权限应对Android的版本变迁文件操作躲不开存储权限。这是Android开发里一个老生常谈但又必须小心处理的点因为不同版本的系统策略变化很大。处理不好就会报Permission denied这种让人头疼的错误。对于 Android 9API 28及以下的设备它们沿用旧的存储模型需要申请经典的WRITE_EXTERNAL_STORAGE运行时权限。但注意从Android 10开始这个权限的作用域变了所以我们要限制它的生效范围。在AndroidManifest.xml里这样声明uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /android:maxSdkVersion28这个属性非常关键它告诉系统只有在API 28及以下的设备上才需要这个权限。这样能避免在更高版本上申请一个不必要的权限影响商店上架。对于 Android 10API 29到 Android 12API 31的设备Google推出了作用域存储Scoped Storage。为了让旧版文件API在过渡期还能用我们需要在application标签里临时启用旧式存储模式application android:requestLegacyExternalStoragetrue ... ... /application注意这个标志在Android 11API 30上默认还有效但到了Android 12API 31及以上如果你的targetSdkVersion是31或更高这个标志就失效了。这意味着对于Android 12的设备你必须完全拥抱作用域存储使用MediaStore或应用专属目录来存放文件。对于 Android 13API 33及以上的设备权限模型又细化了如果需要访问媒体文件图片、视频、音频得申请新的READ_MEDIA_IMAGES等权限。不过如果是操作App自己创建在私有目录或共享目录下的文件通常不需要额外权限。所以一个健壮的存储路径获取函数必须考虑这些版本差异。下面这个工具函数是我在多个项目里用过的它能帮你安全地获取一个可用的文件夹路径import android.content.Context import android.os.Build import android.os.Environment import java.io.File fun getAppSpecificStorageDir(context: Context, folderName: String): File { val baseDir if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10使用应用专属的外部存储目录无需权限 // 例如/storage/emulated/0/Android/data/你的包名/files/Documents/YourFolder context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) } else { // Android 9及以下使用传统的外部存储公共目录 // 需要WRITE_EXTERNAL_STORAGE权限 // 例如/storage/emulated/0/Documents/YourFolder Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) } return File(baseDir, folderName).apply { // 如果文件夹不存在就创建它包括所有必要的父目录 if (!exists()) { mkdirs() } } }用这个函数你就能得到一个适合当前系统版本的、可写的目录。把压缩包生成在这里或者从这里读取文件进行压缩能最大程度避免权限问题。3. 核心实战手把手实现加密压缩与解压理论说再多不如代码来得实在。我们来写一个真正能用、好用的工具类。3.1 基础工具类加密压缩与解压我把这个类设计得简单直接暴露两个核心方法打包和解包。import android.content.Context import net.lingala.zip4j.ZipFile import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.enums.CompressionLevel import net.lingala.zip4j.model.enums.CompressionMethod import net.lingala.zip4j.model.enums.EncryptionMethod import java.io.File class SecureZipManager(private val context: Context) { /** * 将单个文件或整个文件夹加密压缩 * * param sourcePath 源文件或文件夹的绝对路径 * param outputZipPath 要生成的加密ZIP文件的绝对路径 * param password 加密密码不能为空或太简单 * param encryptionMethod 加密算法强烈建议用AES */ fun createEncryptedZip( sourcePath: String, outputZipPath: String, password: String, encryptionMethod: EncryptionMethod EncryptionMethod.AES ) { val sourceFile File(sourcePath) if (!sourceFile.exists()) { throw IllegalArgumentException(源文件或目录不存在: $sourcePath) } // 1. 创建ZipFile对象并传入密码。这是加密的关键一步。 val zipFile ZipFile(outputZipPath, password.toCharArray()) // 2. 配置压缩参数 val parameters ZipParameters().apply { compressionMethod CompressionMethod.DEFLATE // 标准压缩算法 compressionLevel CompressionLevel.NORMAL // 平衡压缩速度和压缩率 isEncryptFiles true // 开启加密 this.encryptionMethod encryptionMethod // 设置加密算法 } // 3. 根据源类型文件或文件夹执行压缩 when { sourceFile.isFile - { zipFile.addFile(sourceFile, parameters) println(已加密压缩单个文件: ${sourceFile.name}) } sourceFile.isDirectory - { zipFile.addFolder(sourceFile, parameters) println(已加密压缩整个文件夹: ${sourceFile.name}) } } } /** * 解压一个加密的ZIP文件 * * param zipFilePath 加密ZIP文件的绝对路径 * param destDirectoryPath 解压目标文件夹的绝对路径 * param password 解压密码必须和压缩时一致 */ fun extractEncryptedZip( zipFilePath: String, destDirectoryPath: String, password: String ) { val zipFile File(zipFilePath) if (!zipFile.exists()) { throw IllegalArgumentException(ZIP文件不存在: $zipFilePath) } val destDir File(destDirectoryPath) if (!destDir.exists()) { destDir.mkdirs() } // 创建ZipFile对象时传入密码用于解密 val zipFileInstance ZipFile(zipFile, password.toCharArray()) // 验证密码是否正确Zip4j在解压时会自动验证 if (!zipFileInstance.isValidZipFile) { throw net.lingala.zip4j.exception.ZipException(无效的ZIP文件或密码错误) } // 执行解压所有文件都会解压到destDirectoryPath下 zipFileInstance.extractAll(destDirectoryPath) println(加密ZIP文件解压完成至: $destDirectoryPath) } }怎么用看个例子假设你的App里有个“备份聊天记录”的功能。// 在Activity或ViewModel中 val zipManager SecureZipManager(this) // 假设聊天记录数据库文件在这里 val dbFile File(getAppSpecificStorageDir(this, ChatDB), messages.db) val backupZipPath File(getAppSpecificStorageDir(this, Backups), chat_backup_20231027.zip).absolutePath val strongPassword MySecurePssw0rd!2023 try { zipManager.createEncryptedZip( sourcePath dbFile.absolutePath, outputZipPath backupZipPath, password strongPassword ) // 提示用户备份成功并提醒其记住密码 showToast(聊天记录已加密备份完成) } catch (e: Exception) { // 处理异常比如文件不存在、密码太弱、存储空间不足等 Log.e(Backup, 备份失败, e) showToast(备份失败: ${e.localizedMessage}) }解压也一样简单// 用户想从备份恢复 val backupZipPath ... // 上面生成的ZIP路径 val restoreDir getAppSpecificStorageDir(this, RestoredChat).absolutePath val userInputPassword ... // 从输入框获取 try { zipManager.extractEncryptedZip(backupZipPath, restoreDir, userInputPassword) showToast(恢复成功) } catch (e: net.lingala.zip4j.exception.ZipException) { // 很可能是密码错误 showToast(解压失败密码错误或文件已损坏。) } catch (e: Exception) { showToast(恢复过程出错: ${e.localizedMessage}) }3.2 进阶功能给压缩过程加上“进度条”当用户压缩一个几百兆的视频文件时如果界面卡死不动他很可能以为App崩溃了。所以提供进度反馈是必须的。Zip4j提供了ProgressMonitor来帮我们。我们来升级一下工具类加入带进度回调的压缩方法import net.lingala.zip4j.progress.ProgressMonitor class AdvancedSecureZipManager(context: Context) : SecureZipManager(context) { /** * 带进度回调的加密压缩适用于大文件 * param progressCallback 回调函数参数为当前已处理字节数总字节数 */ fun createEncryptedZipWithProgress( sourcePath: String, outputZipPath: String, password: String, progressCallback: (Long, Long) - Unit ) { val sourceFile File(sourcePath) val zipFile ZipFile(outputZipPath, password.toCharArray()) val parameters ZipParameters().apply { isEncryptFiles true encryptionMethod EncryptionMethod.AES } // 启动一个协程或线程来轮询进度 // 这里用线程示例实际项目中更推荐用协程 val progressThread Thread { var previousProgress 0L while (true) { val monitor: ProgressMonitor zipFile.progressMonitor when (monitor.state) { ProgressMonitor.State.BUSY - { // 正在处理中计算并回调进度 val current monitor.workCompleted val total monitor.totalWork if (current previousProgress) { progressCallback(current, total) previousProgress current } Thread.sleep(200) // 每200毫秒更新一次避免太频繁 } ProgressMonitor.State.READY - { // 任务完成或尚未开始 break } ProgressMonitor.State.ERROR - { // 发生错误 break } } } } progressThread.start() // 执行压缩操作这会触发进度监控 when { sourceFile.isFile - zipFile.addFile(sourceFile, parameters) sourceFile.isDirectory - zipFile.addFolder(sourceFile, parameters) } // 等待进度线程结束可选 progressThread.join() } }在UI层你可以这样绑定进度条// 在Activity或Fragment中 val progressBar: ProgressBar findViewById(R.id.progressBar) val advancedManager AdvancedSecureZipManager(this) advancedManager.createEncryptedZipWithProgress( sourcePath largeVideoFile.path, outputZipPath backupZipPath, password password ) { current, total - // 这个回调可能在后台线程被调用更新UI要切回主线程 runOnUiThread { progressBar.max total.toInt() progressBar.progress current.toInt() // 也可以更新文本比如“已压缩 85MB/200MB” val text 已压缩 ${current / (1024*1024)}MB/${total / (1024*1024)}MB progressTextView.text text } } // 注意这是一个阻塞调用会卡住UI线程。实际开发中应该用协程或RxJava等切换到后台线程执行。4. 应对超大文件分卷压缩与存储优化4.1 实现分卷压缩突破单文件大小限制有时候你要压缩的是一个巨大的数据库文件或者高清视频单个文件就好几个G。直接压缩成一个ZIP包不仅传输不便在某些文件系统上还可能遇到问题。Zip4j很贴心地支持了分卷压缩可以把一个大压缩包自动切成多个指定大小的小文件。fun createSplitEncryptedZip( sourceFilePath: String, outputZipPath: String, password: String, splitSizeInMB: Long 100 // 默认每卷100MB ) { val sourceFile File(sourceFilePath) val zipFile ZipFile(outputZipPath, password.toCharArray()) // 关键配置开启分卷并设置每卷大小 zipFile.isSplitArchive true // 将MB转换为字节 zipFile.splitLength splitSizeInMB * 1024 * 1024 val parameters ZipParameters().apply { isEncryptFiles true encryptionMethod EncryptionMethod.AES } zipFile.addFile(sourceFile, parameters) // 完成后你会得到类似 db_backup.zip, db_backup.z01, db_backup.z02 的文件 println(分卷压缩完成。) }分卷压缩的结果假设你设置每卷100MB压缩一个450MB的文件你会得到backup.zip(第一个卷也包含ZIP目录信息)backup.z01backup.z02backup.z03backup.z04解压时你只需要操作第一个文件backup.zipZip4j会自动识别并读取所有分卷文件前提是它们都在同一个目录下。4.2 存储优化策略空间与性能的平衡压缩不是免费的它消耗CPU时间和内存。在移动设备上我们需要更精细地控制。1. 压缩级别选择Zip4j提供了多个压缩级别从FASTEST到MAXIMUM。级别越高压缩后文件越小但耗时越长CPU占用也越高。val parameters ZipParameters().apply { compressionLevel when { // 用户急着要文件不大 - 追求速度 needSpeed - CompressionLevel.FASTEST // 网络环境差需要尽量减小体积 - 追求压缩率 needSmallestSize - CompressionLevel.MAXIMUM // 默认情况好的平衡点 else - CompressionLevel.NORMAL } }我的经验是对于文本、日志、数据库这类压缩率很高的文件用MAXIMUM很划算体积能小很多。对于已经是压缩格式的如JPEG图片、MP4视频用FASTEST甚至STORE仅存储不压缩就行因为CPU白忙活一场体积也减不了多少。2. 内存与流式操作压缩特大文件时切忌一次性把整个文件读进内存。Zip4j的addFile方法内部是流式处理的一般没问题。但如果你是从网络流或其他输入流直接压缩可以用addStream方法fun compressStream(inputStream: InputStream, outputZipPath: String, password: String) { val zipFile ZipFile(outputZipPath, password.toCharArray()) val parameters ZipParameters().apply { isEncryptFiles true fileNameInZip received_data.bin // 为流中的数据指定一个文件名 } // 使用use块确保流被正确关闭 zipFile.addStream(inputStream, parameters) }3. 文件筛选与排除压缩文件夹时里面可能有很多临时文件.tmp、缓存文件.cache或者缩略图文件夹Thumbs.db我们不想把它们打包进去。parameters.fileExclusionFilters listOf( FileFilter { file - file.name.endsWith(.tmp) }, FileFilter { file - file.name.endsWith(.log) file.length() 10 * 1024 * 1024 }, // 排除大于10MB的日志 FileFilter { file - file.isHidden } // 排除隐藏文件 )这样配置后addFolder方法就会自动跳过这些文件让你的压缩包更干净。5. 避坑指南常见问题与安全强化用了这么多年Zip4j我踩过的坑也不少这里分享几个最典型的。5.1 密码问题不是随便输几个字就行Zip4j对密码有基本强度要求太简单的密码比如全数字、全字母的短密码会直接抛出ZipException: Password is too weak。这其实是好事逼着我们提升安全性。解决方案在压缩前主动校验密码强度。fun validatePasswordStrength(password: String): Boolean { if (password.length 8) return false var hasUpper false var hasLower false var hasDigit false var hasSpecial false for (ch in password) { when { ch.isUpperCase() - hasUpper true ch.isLowerCase() - hasLower true ch.isDigit() - hasDigit true !ch.isLetterOrDigit() - hasSpecial true // 特殊字符 } } // 要求至少包含其中三种 val criteriaMet listOf(hasUpper, hasLower, hasDigit, hasSpecial).count { it } return criteriaMet 3 } // 使用 val userPassword editText.text.toString() if (!validatePasswordStrength(userPassword)) { showToast(密码需至少8位包含大小写字母、数字和特殊符号中的至少三种) return } // 密码合格继续压缩...对于真正敏感的数据甚至可以考虑在App端用密钥派生函数如PBKDF2对用户输入的密码进行加强后再交给Zip4j不过Zip4j内部已经做了类似处理我们提供足够强的原始密码即可。5.2 文件路径与权限问题问题1源文件找不到。尤其是在Android 10以上如果你试图用绝对路径比如/storage/emulated/0/Download/myfile.pdf去访问其他App创建的文件而没有通过SAF存储访问框架或MediaStoreAPI那肯定会失败。始终使用我们前面写的getAppSpecificStorageDir或通过正确API获取的Uri来访问文件。问题2目标文件无法创建。检查目标目录是否存在且可写。对于应用专属目录通常是可写的对于尝试写入公共目录如Downloads在Android 10以上需要用户通过SAF选择目录或者你的App有管理外部存储的特殊权限。一个健壮的操作模板fun performSafeZipOperation() { try { // 1. 检查源 val sourceFile File(sourcePath) if (!sourceFile.exists() || !sourceFile.canRead()) { throw IOException(无法读取源文件可能不存在或无权限。) } // 2. 检查并准备目标目录 val targetZipFile File(outputZipPath) val parentDir targetZipFile.parentFile parentDir?.mkdirs() // 创建父目录 if (parentDir null || !parentDir.exists() || !parentDir.canWrite()) { throw IOException(无法写入目标目录: ${parentDir?.absolutePath}) } // 3. 如果目标文件已存在确认是否覆盖根据你的业务逻辑 if (targetZipFile.exists()) { // 可以弹出对话框让用户选择这里直接覆盖为例 targetZipFile.delete() } // 4. 执行压缩/解压 // ... 调用Zip4j // 5. 操作后验证 if (targetZipFile.exists() targetZipFile.length() 0) { // 成功 } else { throw IOException(操作后目标文件异常。) } } catch (e: net.lingala.zip4j.exception.ZipException) { // Zip4j特有异常如密码错误、文件损坏 Log.e(ZipError, ZIP操作失败, e) showToast(操作失败: ${e.message}) } catch (e: IOException) { // IO相关异常 Log.e(ZipError, IO错误, e) showToast(文件读写错误: ${e.message}) } catch (e: Exception) { // 其他未知异常 Log.e(ZipError, 未知错误, e) showToast(发生未知错误) } }5.3 多线程与性能默认情况下Zip4j的压缩操作是同步的会阻塞调用线程。对于大文件这肯定会阻塞UI。有几种处理方式方式一手动切换到后台线程。这是最直接的方式用Thread、ExecutorService或者Kotlin协程都行。// 使用协程示例需要引入kotlinx-coroutines viewModelScope.launch(Dispatchers.IO) { // 在IO线程执行耗时压缩操作 zipManager.createEncryptedZip(...) withContext(Dispatchers.Main) { // 回到主线程更新UI showToast(压缩完成) } }方式二启用Zip4j内置的线程模式。Zip4j提供了一种简单的异步设置val zipFile ZipFile(...) zipFile.isRunInThread true // 启用后台线程执行 zipFile.addFile(...) // 这个方法会立即返回操作在后台进行 // 但是这种方式下进度监控和异常捕获会更复杂一些需要设置监听器。 // 对于简单场景我更推荐自己控制线程。内存方面Zip4j 2.x版本在处理大文件时内存控制得不错。但如果你在循环中频繁创建大量ZipFile对象处理小文件记得及时关闭或让它们被回收。虽然ZipFile类没有显式的close方法在操作完成后内部资源会释放但在一个操作完成后将其引用置空有助于GC。最后别忘了测试。特别是边界情况空文件夹、0字节文件、文件名包含特殊字符尤其是中文、不同Android版本下的路径。在真机上多跑跑模拟器上的存储行为有时和真机有差异。把这些坑提前踩平你的“加密压缩”功能才能真正让用户觉得可靠。