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




