1, 使用的是Mumu安卓设备,4G内存
2,debug模式,用android studio写的解压代码,压缩包300兆左右,文件大约10000多左右
3,当解压到3000多的时候整个APP报错,手动在MT当中解压没问题,代码如下:
/**
* 安全地解压ZIP文件到目标目录
*
* 本方法针对大型ZIP文件和大量文件进行了优化,主要改进:
* 1. 添加了路径安全检查,防止ZIP路径遍历攻击
* 2. 增加了资源限制,防止ZIP炸弹攻击
* 3. 优化了内存管理,避免OOM错误
* 4. 完善了异常处理机制
* 5. 添加了进度反馈和状态更新
*
* @param zipFile ZIP文件
* @param targetDirectory 目标目录
* @throws IOException 解压过程中可能出现的IO异常
* @throws SecurityException 如果检测到潜在的安全风险(如路径遍历)
*/
@SuppressLint("SetWorldReadable")
private void unzipFile(File zipFile, File targetDirectory) throws IOException {
// 1. 验证输入参数
if (zipFile == null || !zipFile.exists() || !zipFile.isFile()) {
throw new FileNotFoundException("ZIP文件不存在: " + (zipFile != null ? zipFile.getAbsolutePath() : "null"));
}
if (targetDirectory == null || !targetDirectory.exists()) {
if (targetDirectory != null && !targetDirectory.mkdirs()) {
throw new IOException("无法创建目标目录: " + targetDirectory.getAbsolutePath());
}
}
// 2. 设置安全限制参数
final int MAX_FILES = 5000; // 最大文件数量限制
final long MAX_TOTAL_SIZE = 2L * 1024 * 1024 * 1024; // 总解压大小限制(2GB)
final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 单个文件大小限制(500MB)
final int MAX_PATH_DEPTH = 30; // 最大路径深度,防止ZIP炸弹
long totalUnzippedSize = 0;
int fileCount = 0;
// 3. 使用try-with-resources确保资源正确关闭
try (ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(new FileInputStream(zipFile), 8192))) {
ZipEntry entry;
byte[] buffer = new byte[8192];
int count;
// 4. 逐个处理ZIP条目
while ((entry = zis.getNextEntry()) != null) {
// 5. 安全检查 - 防止ZIP路径遍历攻击
String entryName = entry.getName();
if (isPathTraversal(entryName)) {
throw new SecurityException("检测到路径遍历攻击: " + entryName);
}
// 6. 检查文件数量限制
if (++fileCount > MAX_FILES) {
throw new IOException("ZIP文件包含过多文件(超过" + MAX_FILES + "个)");
}
// 7. 检查文件大小限制
if (entry.getSize() > MAX_FILE_SIZE) {
throw new IOException("文件过大(超过500MB): " + entryName);
}
// 8. 检查总解压大小限制
if (entry.getSize() > 0) { // 有些条目可能没有大小信息
totalUnzippedSize += entry.getSize();
if (totalUnzippedSize > MAX_TOTAL_SIZE) {
throw new IOException("总解压大小超过限制(2GB)");
}
}
// 9. 构建目标文件路径
File targetFile = new File(targetDirectory, entryName);
// 10. 验证目标文件是否在目标目录内
if (!isWithinDirectory(targetFile, targetDirectory)) {
throw new SecurityException("文件路径超出目标目录范围: " + entryName);
}
// 11. 处理目录条目
if (entry.isDirectory()) {
// 检查路径深度
int depth = entryName.split("[/\\\\]").length;
if (depth > MAX_PATH_DEPTH) {
throw new IOException("路径深度超过限制: " + entryName);
}
if (!targetFile.mkdirs() && !targetFile.isDirectory()) {
throw new IOException("无法创建目录: " + targetFile.getAbsolutePath());
}
continue;
}
// 12. 确保父目录存在
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("无法创建父目录: " + parentDir.getAbsolutePath());
}
// 13. 写入文件内容(分块处理,避免内存问题)
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
long bytesWritten = 0;
while ((count = zis.read(buffer)) != -1) {
fos.write(buffer, 0, count);
bytesWritten += count;
// 14. 检查实际写入大小是否超过条目声明的大小
if (entry.getSize() > 0 && bytesWritten > entry.getSize()) {
throw new IOException("文件大小超出预期: " + entryName);
}
// 15. 每解压一定数据后更新UI状态
if (bytesWritten % (1024 * 1024) == 0) { // 每1MB更新一次
final String status = "解压中... " + entryName + " (" + (bytesWritten / 1024) + " KB)";
runOnUiThread(new Runnable() {
@Override
public void run() {
tvStatus.setText(status);
}
});
}
}
// 16. 设置文件权限
if (!targetFile.setReadable(true, false) ||
!targetFile.setWritable(true, false)) {
Log.w("Unzip", "无法设置文件权限: " + targetFile.getName());
}
} catch (IOException e) {
// 17. 清理部分写入的文件
if (targetFile.exists() && !targetFile.delete()) {
Log.e("Unzip", "无法删除部分写入的文件: " + targetFile.getName());
}
throw new IOException("写入文件失败: " + entryName, e);
}
// 18. 定期更新UI状态(每5个文件)
if (fileCount % 5 == 0) {
final int finalFileCount = fileCount;
runOnUiThread(new Runnable() {
@Override
public void run() {
tvStatus.setText("已解压 " + finalFileCount + " 个文件...");
}
});
}
}
// 19. 解压完成更新状态
runOnUiThread(new Runnable() {
@Override
public void run() {
tvStatus.setText("解压完成! 共解压 " + fileCount + " 个文件");
}
});
} catch (SecurityException se) {
throw se; // 重新抛出安全异常
} catch (Exception e) {
// 20. 详细记录错误信息
Log.e("Unzip", "解压过程中出错", e);
throw new IOException("解压失败: " + e.getMessage(), e);
}
}
/**
* 检查路径是否包含路径遍历(如"../")
*
* @param path 要检查的路径
* @return 如果是路径遍历则返回true
*/
private boolean isPathTraversal(String path) {
// 规范化路径,将所有分隔符转换为"/"
String normalizedPath = path.replace(File.separator, "/");
// 检查是否包含"../"或以"../"开头
if (normalizedPath.contains("../") || normalizedPath.startsWith("../")) {
return true;
}
// 检查是否包含"./"(虽然通常安全,但可能是恶意尝试)
if (normalizedPath.contains("./") && !normalizedPath.matches("[./]+")) {
return true;
}
// 检查绝对路径
if (normalizedPath.startsWith("/") || normalizedPath.matches("^[a-zA-Z]:[/\\\\].*")) {
return true;
}
return false;
}
/**
* 检查文件是否在指定目录内
*
* @param file 要检查的文件
* @param directory 目标目录
* @return 如果文件在目录内则返回true
*/
private boolean isWithinDirectory(File file, File directory) {
try {
// 获取规范化路径
String canonicalFile = file.getCanonicalPath();
String canonicalDir = directory.getCanonicalPath();
// 确保目录路径以分隔符结尾
if (!canonicalDir.endsWith(File.separator)) {
canonicalDir += File.separator;
}
// 检查文件路径是否以目录路径开头
return canonicalFile.startsWith(canonicalDir);
} catch (IOException e) {
// 无法获取规范化路径,保守起见认为不安全
Log.e("Unzip", "路径规范化失败", e);
return false;
}
}