You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android中如何通过Storage Volume(SAF)解压文件并保留目录结构?

如何通过Storage Access Framework(SAF)在Android 7.0+解压Zip文件到SD卡并保留目录结构

你已经成功拿到了ZipInputStream,接下来的核心就是利用DocumentFile来处理目录创建和文件写入——毕竟在SAF体系下,我们没法直接操作传统的File对象。下面是完整的实现方案,包含多级目录的递归创建和文件内容的高效写入:

完整实现代码

// 前提:你已经通过SAF获取到目标存储目录的DocumentFile实例
DocumentFile targetRootDir = ...; 
// 你已经初始化好的ZipInputStream(来自getContentResolver().openInputStream())
ZipInputStream zipIn = ...; 

ZipEntry zipEntry;
try {
    while ((zipEntry = zipIn.getNextEntry()) != null) {
        String entryPath = zipEntry.getName();
        
        // 跳过空条目
        if (entryPath.trim().isEmpty()) {
            zipIn.closeEntry();
            continue;
        }

        // 处理目录条目:创建对应的多级目录结构
        if (zipEntry.isDirectory()) {
            createNestedDirectories(targetRootDir, entryPath);
            zipIn.closeEntry();
            continue;
        }

        // 处理文件条目:先构建父目录,再创建文件并写入内容
        int lastSlashPos = entryPath.lastIndexOf('/');
        String parentDirPath = lastSlashPos != -1 ? entryPath.substring(0, lastSlashPos) : "";
        String fileName = lastSlashPos != -1 ? entryPath.substring(lastSlashPos + 1) : entryPath;

        // 创建文件所在的父目录层级
        DocumentFile parentDir = createNestedDirectories(targetRootDir, parentDirPath);
        if (parentDir == null) {
            Log.e("ZipUnzip", "Failed to create parent dir for: " + entryPath);
            zipIn.closeEntry();
            continue;
        }

        // 在父目录下创建目标文件
        DocumentFile targetFile = parentDir.createFile(getFileMimeType(fileName), fileName);
        if (targetFile == null) {
            Log.e("ZipUnzip", "Failed to create file: " + entryPath);
            zipIn.closeEntry();
            continue;
        }

        // 写入文件内容(用try-with-resources自动关流)
        try (OutputStream out = getContentResolver().openOutputStream(targetFile.getUri());
             BufferedOutputStream bos = new BufferedOutputStream(out)) {

            byte[] buffer = new byte[8192]; // 8KB缓冲是比较均衡的选择
            int readLen;
            while ((readLen = zipIn.read(buffer)) != -1) {
                bos.write(buffer, 0, readLen);
            }
            bos.flush();
        } catch (IOException e) {
            Log.e("ZipUnzip", "Error writing file: " + entryPath, e);
            // 写入失败时清理空文件
            targetFile.delete();
        }

        zipIn.closeEntry();
    }
} catch (IOException e) {
    Log.e("ZipUnzip", "Error processing zip archive", e);
} finally {
    // 确保关闭ZipInputStream
    try {
        zipIn.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/**
 * 递归创建多级目录(SAF不支持直接创建多级,必须逐级创建)
 * @param rootDir 根目录DocumentFile
 * @param dirPath 要创建的目录路径(用/分隔)
 * @return 最终创建的目录DocumentFile,失败返回null
 */
private DocumentFile createNestedDirectories(DocumentFile rootDir, String dirPath) {
    if (dirPath.isEmpty()) {
        return rootDir;
    }

    DocumentFile currentDir = rootDir;
    String[] dirSegments = dirPath.split("/");
    for (String segment : dirSegments) {
        if (segment.isEmpty()) {
            continue;
        }
        // 检查当前目录下是否已存在该子目录
        DocumentFile childDir = currentDir.findFile(segment);
        if (childDir == null || !childDir.isDirectory()) {
            // 不存在则创建
            childDir = currentDir.createDirectory(segment);
        }
        if (childDir == null) {
            // 创建失败,终止并返回null
            return null;
        }
        currentDir = childDir;
    }
    return currentDir;
}

/**
 * 根据文件名获取MIME类型(DocumentFile.createFile需要此参数)
 */
private String getFileMimeType(String fileName) {
    String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
    if (extension != null) {
        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
    }
    // 默认返回二进制流类型
    return "application/octet-stream";
}

关键细节说明

  1. 多级目录创建逻辑:SAF的createDirectory()只能创建当前目录的直接子目录,所以我们需要把ZipEntry的路径按/拆分成单个目录段,逐级检查并创建每一层目录——这就是createNestedDirectories方法的核心作用。
  2. 目录与文件区分:通过zipEntry.isDirectory()判断条目类型,目录直接创建层级,文件则先处理父目录再创建文件。
  3. 流的安全处理:使用try-with-resources语法自动关闭输入输出流,避免手动关流遗漏导致的资源泄漏。
  4. 错误容错:每一步操作都加了失败判断,创建目录/文件失败时会打印日志,写入失败会自动删除空文件,避免遗留无效文件。
  5. MIME类型适配DocumentFile.createFile()必须传入MIME类型,我们通过文件名后缀自动匹配,找不到对应类型时用默认的二进制流类型兜底。

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

火山引擎 最新活动