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

使用GligarPicker获取多存储路径图片后,复制至应用级文件夹时FileNotFoundException问题解决

问题分析与修复方案

你遇到的EISDIR错误本质是把应用缓存目录本身当成了要写入的文件,再加上代码里错误地调用了file.mkdir()把目标路径创建成了目录,后续尝试用FileOutputStream打开这个目录时自然会报错。咱们一步步拆解问题,然后给出修复后的完整代码:

核心错误点

  1. 路径构造错误:你构造的File对象如果处理不当(比如getFileName返回空),会直接指向externalCacheDir目录,而不是目录下的具体文件;
  2. 多余的mkdir()调用:file.mkdir()会把目标路径创建为目录,之后createNewFile()其实会失效(因为已经是目录了),最后用FileOutputStream打开目录必然触发EISDIR;
  3. Uri处理不够通用:直接拼接file://的方式不适用于所有场景(比如图片来自MediaStore时,Uri格式是content://),可能导致后续流获取失败。

修复后的完整代码

// 遍历用户选择的图片列表
imagesList.forEach { imagePath ->
    try {
        // 1. 正确构造Uri:如果路径已经以file://开头就直接用,否则补全前缀
        val uri = Uri.parse(if (imagePath.startsWith("file://")) imagePath else "file://$imagePath")
        
        // 2. 安全获取文件名:避免为空的情况,用时间戳生成临时文件名兜底
        val fileName = requireContext().contentResolver.getFileName(uri) 
            ?: "upload_image_${System.currentTimeMillis()}.jpeg"
        
        // 3. 构造目标文件:确保是externalCacheDir目录下的具体文件,而非目录本身
        val targetFile = File(requireContext().externalCacheDir, fileName)
        
        // 4. 用ContentResolver打开输入流(支持所有Uri类型,比直接操作FileDescriptor更通用)
        requireContext().contentResolver.openInputStream(uri)?.use { inputStream ->
            // 5. 打开输出流写入文件,use块会自动关闭流,避免资源泄漏
            FileOutputStream(targetFile).use { outputStream ->
                inputStream.copyTo(outputStream, bufferSize = 8192) // 指定缓冲大小提升效率
            }
        }

        // 这里可以调用Retrofit上传逻辑,比如把targetFile转换成MultipartBody.Part
        val uploadPart = createMultipartUploadPart(targetFile)
        // uploadToServer(uploadPart)

    } catch (e: IOException) {
        e.printStackTrace()
        // 这里可以给用户提示复制失败,比如Toast.makeText(requireContext(), "图片复制失败", Toast.LENGTH_SHORT).show()
    }
}

// 扩展函数:从Uri中获取文件名(兼容file://和content://类型的Uri)
fun ContentResolver.getFileName(uri: Uri): String? {
    // 先尝试从MediaStore查询文件名(适用于content://类型的Uri)
    val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME)
    query(uri, projection, null, null, null)?.use { cursor ->
        if (cursor.moveToFirst()) {
            return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
        }
    }
    // 如果查询失败,从Uri路径中提取文件名(适用于file://类型的Uri)
    return uri.path?.let { File(it).name }
}

// 辅助函数:将File转换为Retrofit所需的MultipartBody.Part
fun createMultipartUploadPart(file: File): MultipartBody.Part {
    val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
    return MultipartBody.Part.createFormData("images", file.name, requestBody)
}

关键优化点

  • 移除mkdir()调用:FileOutputStream会自动创建不存在的文件(前提是父目录externalCacheDir存在,这个目录是系统为应用自动创建的,无需手动创建);
  • 用use块管理流:Kotlin的use函数会在代码块执行完毕后自动关闭流,避免资源泄漏;
  • 通用的Uri处理:通过ContentResolver.openInputStream可以兼容file://和content://等多种Uri类型,适配更多图片来源场景;
  • 文件名兜底逻辑:避免因getFileName返回空导致路径错误,用时间戳生成唯一文件名;
  • 指定缓冲大小:copyTo时指定缓冲大小可以提升文件复制的效率。

额外说明

如果你的需求必须使用FileDescriptor(比如需要更底层的操作),可以调整输入流的获取方式,但核心还是要确保目标路径是具体文件而非目录:

val parcelFileDescriptor = requireContext().contentResolver.openFileDescriptor(uri, "r") ?: return
val fileDescriptor = parcelFileDescriptor.fileDescriptor
FileInputStream(fileDescriptor).use { inputStream ->
    FileOutputStream(targetFile).use { outputStream ->
        inputStream.copyTo(outputStream)
    }
}
parcelFileDescriptor.close() // 注意手动关闭ParcelFileDescriptor,或者用use块包裹

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 11:24:14