问题背景:为什么 ZIP 会乱码?
ZIP 是一个"历史包袱很重"的压缩格式,早期并没有规定统一的文件名编码方式。
常见的情况是:
系统 | 常用编码 |
---|---|
Windows | GBK |
macOS / Linux | UTF-8 |
因此:
-
Windows 下压的包,用 UTF-8 解压会乱码;
-
macOS 下压的包,用 GBK 解压也会乱码;
-
而 ZIP 文件内部并没有强制说明使用了哪种编码。
相比之下:
-
7z → 文件名标准使用 UTF-16,几乎不会乱码;
-
RAR → 新版写了 Unicode 信息,问题较少;
-
ZIP → 完全靠猜。
👉 所以:文件名乱码几乎是 ZIP 独有的老问题。
遇到问题1:出现乱码

原因分析
最开始我写的时候,是这样写的:
Kotlin
val zipFile = ZipFile(file)
zipFile.charset = Charset.forName("GBK")
这意味着无论压缩包来源于哪里,都强制使用 GBK 解码。但如果该文件是 macOS 或 Linux 压缩的(即 UTF-8 编码),文件名就会乱码。
解决方案
思路是先用 GBK 尝试解压,如果检测出乱码,再切换到 UTF-8。
如何判断有没有乱码,我就通过文件名称中是否存在"�"去判断的
当然这种方式后面遇到了别的问题,所以不适合
遇到问题2:混合编码(部分 GBK,部分 UTF-8)
下面两个图片分别展示:同一个压缩包文件使用不同编码的情况

可以看到使用GBK的话,上半部分乱码,下半部分正常
使用UTF-8的话,上半部分正常,下半部分乱码
原因
有些压缩包内部的文件名并不是统一编码的,比如:
-
一部分文件使用GBK;
-
一部分文件使用UTF-8
这种情况如果你给整个 zipFile
设置一种编码,就必定会有部分文件乱码。
解决方案:逐文件判断编码
不再给 zipFile
设置统一编码,而是 在读取每个文件头时动态判断编码:
通过 fileHeader.isFileNameUTF8Encoded 能判断哪些用UTF-8,哪些用GBK,如果要使用GBK,就从Cp437转换到GBK
修改:删除给zipfile设置的统一编码,改为每个文件头都判断
Kotlin
val zipFile = ZipFile(file)
// 删除下面
// zipFile.charset = Charset.forName("GBK")
Kotlin
private fun getDecodedFileName(fileHeader: FileHeader): String {
var result = fileHeader.fileName
try {
if (fileHeader.isFileNameUTF8Encoded) {
LogUtil.d(TAG, "UTF-8,不用处理")
} else {
LogUtil.d(TAG, "GBK 编码")
result = String(
fileHeader.fileName.toByteArray(Charset.forName("Cp437")),
Charset.forName("GBK")
)
}
} catch (e: Exception) {
LogUtil.e(TAG, "解码文件名时发生错误: ${e.message}")
}
return result
}
为什么要从Cp437转换到GBK?Zip4j的底层编码是什么样的?
Zip4j库 的解码优先级:
a. 如果开发者手动设置了编码,则使用开发者设置的编码;
b. 否则如果文件头带 UTF-8 标志,则使用 UTF-8 解码;
c. 否则使用 CP437;
上述方案满足了大部分情况,但是满足不了下面的mac操作系统压缩的文件的情况
遇到问题3:macOS 压缩包的特殊情况
下面是同一个mac压缩包里面的文件,使用不同编码的情况
原因
macOS 创建的 ZIP 文件中,虽然文件名实际使用 UTF-8 编码,但它 没有在文件头中设置 UTF-8 标志 。
这导致:
Kotlin
fileHeader.isFileNameUTF8Encoded == false
从而错误地使用 GBK 解码 → 文件名乱码。
解决方案:检测是否是 Mac 压缩包
我发现几乎所有的mac压缩的压缩包,都会包含一个隐藏文件夹"__MACOSX/",所以我通过判断有没有这个文件夹,来区分是不是Mac操作系统压缩的
判断mac操作系统的方法:
Kotlin
private fun isMacZipFile(zipFile: ZipFile): Boolean {
val fileHeaders: List<FileHeader> = zipFile.fileHeaders
for (fileHeader in fileHeaders) {
val name = fileHeader.fileName
if (name.startsWith("__MACOSX/")) {
return true
}
}
return false
}
所以最终的解决方案如下
最终解决方案
Kotlin
/**
* 获取正确编码的文件名,解决文件名乱码问题
*
* 逻辑:
* 1. Zip4j库 的解码优先级:
* a. 如果开发者手动设置了编码,则使用开发者设置的编码;
* b. 否则如果文件头带 UTF-8 标志,则使用 UTF-8 解码;
* c. 否则使用 CP437;
* 2. macOS 生成的 ZIP 文件未设置 UTF-8 标志,但文件名实际是 UTF-8;
* 3. 其他系统根据UTF-8标识设置
*/
private fun getDecodedFileName(isMacZipFile: Boolean, fileHeader: FileHeader): String {
var result = fileHeader.fileName
try {
when {
isMacZipFile -> {
// mac 的直接设置 UTF_8
result = String(
fileHeader.fileName.toByteArray(Charset.forName("Cp437")),
Charsets.UTF_8
)
}
!fileHeader.isFileNameUTF8Encoded -> {
// 非 mac 且 没有 UTF-8 标识:直接使用 GBK
result = String(
fileHeader.fileName.toByteArray(Charset.forName("Cp437")),
Charset.forName("GBK")
)
}
// 有 UTF-8 标识的,Zip4j 已正确用 UTF-8 处理
}
} catch (e: Exception) {
LogUtil.e(TAG, "解码文件名时发生错误: ${e.message}")
}
return result
}
private fun isMacZipFile(zipFile: ZipFile): Boolean {
val fileHeaders: List<FileHeader> = zipFile.fileHeaders
for (fileHeader in fileHeaders) {
val name = fileHeader.fileName
if (name.startsWith("__MACOSX/")) {
return true
}
}
return false
}