2501_93134210 2025-08-31 23:34 采纳率: 0%
浏览 8

安卓默认解压缩代码,大文件解压崩溃问题

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;
    }
}

  • 写回答

5条回答 默认 最新

  • 檀越@新空间 2025-08-31 23:35
    关注

    晚上好🌙🌙🌙
    本答案参考通义千问

    在您提供的代码中,解压到3000多个文件时出现崩溃,这可能是因为以下几个原因导致的:


    一、问题分析

    1. 内存不足(OOM)
      在Android设备上,尤其是低内存设备(如4GB内存),如果使用不当的资源管理方式,可能会导致内存溢出(Out of Memory, OOM)

    2. 未处理大文件或大量文件
      虽然代码中有部分限制(如MAX_FILES = 5000),但实际压缩包有10000多个文件,超过了这个限制,可能导致程序崩溃。

    3. 没有正确关闭流或释放资源
      如果某些流没有被正确关闭,或者资源未被释放,可能导致内存泄漏或资源耗尽。

    4. ZIP文件结构异常或损坏
      虽然MT工具可以解压,但可能是由于某些特殊格式或错误,导致Java标准库无法处理。


    二、解决方案

    1. 增加文件数量和大小限制

    您的代码中设置了MAX_FILES = 5000,但压缩包有10000个文件,建议适当提高该值。

    final int MAX_FILES = 10000; // 根据实际需求调整
    

    同时,可以考虑动态判断文件数量,而不是硬编码限制。


    2. 优化内存管理

    确保所有流都被正确关闭,并避免不必要的对象创建。

    • 使用try-with-resources 确保所有资源自动关闭。
    • 分块读取数据:您已经在使用缓冲区,这是正确的做法。

    3. 添加日志输出,定位崩溃位置

    在关键步骤添加日志,帮助定位崩溃发生的位置。

    Log.d("Unzip", "Processing entry: " + entryName);
    

    4. 使用更高效的 ZIP 解压库

    Android 自带的 ZipInputStream 可能不够高效,特别是对于大文件。您可以考虑使用第三方库,例如:

    这些库通常对内存和性能进行了优化。


    5. 修改后的完整代码示例

    以下是修改后的解压函数,包含优化和增强功能:

    @SuppressLint("SetWorldReadable")
    private void unzipFile(File zipFile, File targetDirectory) throws IOException {
        if (zipFile == null || !zipFile.exists() || !zipFile.isFile()) {
            throw new FileNotFoundException("ZIP文件不存在: " + zipFile.getAbsolutePath());
        }
    
        if (targetDirectory == null || !targetDirectory.exists()) {
            if (!targetDirectory.mkdirs()) {
                throw new IOException("无法创建目标目录: " + targetDirectory.getAbsolutePath());
            }
        }
    
        final int MAX_FILES = 10000;
        final long MAX_TOTAL_SIZE = 5L * 1024 * 1024 * 1024; // 5GB
        final long MAX_FILE_SIZE = 1000 * 1024 * 1024; // 1GB
        final int MAX_PATH_DEPTH = 30;
    
        long totalUnzippedSize = 0;
        int fileCount = 0;
    
        try (ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(zipFile), 8192))) {
    
            ZipEntry entry;
            byte[] buffer = new byte[8192];
            int count;
    
            while ((entry = zis.getNextEntry()) != null) {
                String entryName = entry.getName();
    
                // 防止路径遍历攻击
                if (isPathTraversal(entryName)) {
                    throw new SecurityException("检测到路径遍历攻击: " + entryName);
                }
    
                if (++fileCount > MAX_FILES) {
                    throw new IOException("ZIP文件包含过多文件(超过" + MAX_FILES + "个)");
                }
    
                if (entry.getSize() > MAX_FILE_SIZE) {
                    throw new IOException("文件过大(超过1GB): " + entryName);
                }
    
                if (entry.getSize() > 0) {
                    totalUnzippedSize += entry.getSize();
                    if (totalUnzippedSize > MAX_TOTAL_SIZE) {
                        throw new IOException("总解压大小超过限制(5GB)");
                    }
                }
    
                File targetFile = new File(targetDirectory, entryName);
    
                if (!isWithinDirectory(targetFile, targetDirectory)) {
                    throw new SecurityException("文件路径超出目标目录范围: " + entryName);
                }
    
                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;
                }
    
                File parentDir = targetFile.getParentFile();
                if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
                    throw new IOException("无法创建父目录: " + parentDir.getAbsolutePath());
                }
    
                try (FileOutputStream fos = new FileOutputStream(targetFile)) {
                    while ((count = zis.read(buffer)) != -1) {
                        fos.write(buffer, 0, count);
                    }
                }
            }
        } catch (IOException e) {
            Log.e("Unzip", "解压失败: ", e);
            throw e;
        }
    }
    

    三、附加建议

    1. 检查是否有异常抛出

    确保在代码中捕获并打印所有异常,方便排查。

    2. 使用 Android 的 AsyncTaskWorkManager 异步执行

    对于大文件解压,建议使用异步任务,避免阻塞主线程。

    3. 增加内存监控

    在 Android Studio 中使用 Memory Profiler 工具,查看内存使用情况,找出潜在的内存泄漏点。


    四、总结

    | 问题 | 解决方案 | |------|----------| | 内存不足 | 使用 try-with-resources 确保资源释放,优化缓冲区大小 | | 文件数量限制 | 提高 MAX_FILES 值或动态判断 | | 大文件处理 | 使用更高效的 ZIP 库(如 Compress) | | 日志定位 | 添加详细日志输出,定位崩溃位置 |


    如果您仍然遇到问题,请提供完整的错误日志,我可以进一步帮助您定位具体原因。

    评论

报告相同问题?

问题事件

  • 创建了问题 8月31日