You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中如何将Retrofit返回的ByteArray写入Excel并存储到SD卡?

解决Retrofit下载Excel文件为空且格式无效的问题

问题分析

你遇到的0KB空文件和格式错误提示(翻译后:

Excel无法打开文件“ExcelTest.xlsx”,因为文件格式或文件扩展名无效。请验证文件未损坏,且文件扩展名与文件格式匹配。
),核心是代码里的几个关键错误:

  • 文件路径处理错误:直接拼接字符串生成文件路径,且打开文件时传入的是目录Uri而非文件Uri
  • 空安全隐患:it.data?.bytes()如果it.data为null,会写入空内容导致文件0KB
  • 高版本Android Uri兼容问题:7.0+使用Uri.fromFile会触发FileUriExposedException,同时无法正确被其他应用访问
  • 异常捕获范围过窄:仅捕获FileNotFoundException,无法排查其他写入或IO错误

修正后的代码

1. 处理文件写入逻辑(修复路径、空安全、流写入优化)

try {
    mContext?.let { context ->
        // 获取外部存储的文档目录,比PICTURES更适合存放Excel文件
        val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) ?: 
            context.getExternalFilesDir(null) // 备选目录
        
        // 创建目标文件对象,避免字符串拼接的路径错误
        val excelFile = File(parentDir, "ExcelTest.xlsx")
        
        // 确保父目录存在
        if (!parentDir.exists()) {
            parentDir.mkdirs()
        }

        // 检查ResponseBody非空,用流逐块写入(避免大文件OOM)
        it.data?.let { responseBody ->
            FileOutputStream(excelFile).use { outputStream ->
                responseBody.byteStream().use { inputStream ->
                    // 用缓冲区复制流,效率更高
                    val buffer = ByteArray(4096)
                    var bytesRead: Int
                    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        outputStream.write(buffer, 0, bytesRead)
                    }
                    outputStream.flush()
                }
            }
        } ?: run {
            // 处理ResponseBody为空的情况
            Log.e("ExcelDownload", "ResponseBody is null")
            return@let
        }

        // 打开Excel文件的正确逻辑
        openExcelFile(context, excelFile)
    }
} catch (e: IOException) {
    // 捕获所有IO异常并打印,方便调试
    Log.e("ExcelDownload", "Write file failed", e)
}

2. 实现兼容高版本的文件打开方法

private fun openExcelFile(context: Context, file: File) {
    val fileUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // Android 7.0+ 使用FileProvider生成Uri
        FileProvider.getUriForFile(
            context,
            "${context.packageName}.fileprovider", // 对应AndroidManifest里的authorities
            file
        )
    } else {
        Uri.fromFile(file)
    }

    val intent = Intent(Intent.ACTION_VIEW).apply {
        setDataAndType(fileUri, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
        flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
    }

    // 检查是否有应用能处理该Intent
    if (intent.resolveActivity(context.packageManager) != null) {
        context.startActivity(intent)
    } else {
        Log.e("ExcelDownload", "No app available to open Excel file")
    }
}

3. 配置FileProvider(AndroidManifest.xml)

如果是Android 7.0+,需要在Manifest里添加FileProvider配置:

<application ...>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

然后在res/xml目录下创建file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="external_files" path="." />
</paths>

关键优化点说明

  • 使用File对象而非字符串拼接路径,避免路径格式错误
  • 用use函数自动关闭流,无需手动调用close(),防止资源泄漏
  • 逐块复制流而非bytes()方法,避免大文件加载到内存导致OOM
  • 兼容Android 7.0+的FileProvider,解决Uri暴露问题
  • 扩大异常捕获范围并打印日志,便于定位问题
  • 检查目标目录是否存在,避免因目录不存在导致写入失败

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 03:01:06