You need to enable JavaScript to run this app. Android 11+ Scoped Storage适配:Retrofit选图上传崩溃问题咨询
优惠活动
大模型
产品
解决方案
定价
更多

Android 11+ Scoped Storage适配:Retrofit选图上传崩溃问题咨询

问题根因定位

你遇到的Failed to build unique file崩溃完全是Scoped Storage适配逻辑失效导致的,核心问题点如下:

  • Android 11起requestLegacyExternalStorage配置完全失效,应用不能再直接通过Environment.getExternalStorageDirectory()拼路径向公共Pictures目录写文件
  • 旧逻辑中调用的MediaStore.Images.Media.insertImage()方法在Android11上存在严格的重名校验,不允许应用随意在公共目录生成无唯一标识的媒体文件,这就是直接触发崩溃的代码位置
  • 试图通过getPath()查询MediaStore.Images.Media.DATA字段获取文件绝对路径的逻辑在Android10+上已经完全废弃,该字段返回的路径要么为空要么无访问权限,根本无法用于文件读取或上传
  • 整个旧流程中存在大量绕MediaStore、直接操作裸文件路径的逻辑,完全不符合Scoped Storage的访问规则

具体适配改造方案

所有改造逻辑全程不需要申请特殊存储权限,也完全不受Scoped Storage规则限制,可直接复用现有FileProvider、Retrofit上传逻辑。

1. 废弃所有旧的路径操作逻辑

  • 删掉原来直接拼公共Pictures路径的setimagepath()方法,替换为直接使用应用私有外部缓存目录的逻辑——私有目录文件不需要任何权限,应用有完全读写权,也不会触发Scoped Storage校验:
// 生成相机拍摄的临时文件,全程不需要存储权限
private File getCameraTempFile() {
    String fileName = "IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".jpg";
    // 路径为 /sdcard/Android/data/你的包名/cache/temp_images
    File storageDir = new File(getActivity().getExternalCacheDir(), "temp_images");
    if (!storageDir.exists()) {
        storageDir.mkdirs();
    }
    return new File(storageDir, fileName);
}
  • 整个删掉旧的getImageUri(Bitmap)方法,这个方法依赖MediaStore.insertImage写公共目录,是当前崩溃的直接来源
  • 整个删掉旧的getPath()方法,永远不要再尝试通过MediaStore.DATA字段拿绝对路径——Retrofit上传完全不需要依赖系统媒体库的裸路径

2. 修正相机拍照调用逻辑

保留现有FileProvider的授权逻辑,只替换临时文件的来源,同时删掉高版本上会导致图片压缩成缩略图的无效参数:

private void takePicture() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    try {
        // 用私有目录的临时文件作为拍照输出目标
        mFileTemp = getCameraTempFile();
        // 用FileProvider生成可授权给相机的Uri,和原有逻辑完全兼容
        Uri mImageCaptureUri = FileProvider.getUriForFile(
            getContext(),
            BuildConfig.APPLICATION_ID + ".provider",
            mFileTemp
        );
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageCaptureUri);
        // 给Uri临时授权写权限,确保相机可以正常保存照片
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
    } catch (ActivityNotFoundException e) {
        Log.e("Camera", "无法启动系统相机", e);
    }
}

3. 优化相册选图的Intent逻辑

替换原来的ACTION_PICK为更规范的文档选择Intent,同时给选中的Uri加永久读权限,避免后续配置变更后Uri失效:

private void openGallery() {
    Intent photoPickerIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    photoPickerIntent.addCategory(Intent.CATEGORY_OPENABLE);
    photoPickerIntent.setType("image/*");
    // 允许对选中的Uri持有永久读权限,不受系统权限回收影响
    photoPickerIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    startActivityForResult(photoPickerIntent, REQUEST_CODE_GALLERY);
}

4. 重写onActivityResult的图片处理流程

核心思路是:拿到图片Uri后,直接把图片流复制到应用私有缓存目录,后续的图片压缩、显示、Retrofit上传全部基于这个私有目录文件,完全不需要绕MediaStore转一圈,也不会触发任何权限问题:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode != RESULT_OK) return;

    Uri sourceImageUri = null;
    // 处理相册选图返回
    if (requestCode == REQUEST_CODE_GALLERY && data != null) {
        sourceImageUri = data.getData();
        // 给选中的Uri添加持久化读权限
        try {
            getActivity().getContentResolver().takePersistableUriPermission(
                sourceImageUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION
            );
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }
    // 处理相机拍照返回,直接使用之前创建的临时文件
    else if (requestCode == REQUEST_CODE_TAKE_PICTURE) {
        sourceImageUri = Uri.fromFile(mFileTemp);
    }

    if (sourceImageUri == null) return;

    try {
        // 把图片流复制到私有缓存目录作为统一的上传源文件
        File uploadTempFile = new File(getActivity().getExternalCacheDir(), "upload_temp.jpg");
        InputStream inputStream = getActivity().getContentResolver().openInputStream(sourceImageUri);
        FileOutputStream outputStream = new FileOutputStream(uploadTempFile);
        byte[] buffer = new byte[4096];
        int readLen;
        while ((readLen = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, readLen);
        }
        inputStream.close();
        outputStream.close();

        // 后续流程全部用这个私有目录的文件路径,完全有访问权限
        filepath = uploadTempFile.getAbsolutePath();
        // 原有的图片显示逻辑可以直接复用,不需要修改
        Utils.loadImageRound(getActivity(), profileImage, filepath);
        Log.d(TAG, "onActivityResult: 可用于上传的文件路径 = " + filepath);

    } catch (Exception e) {
        e.printStackTrace();
        Toast.makeText(getActivity(), "图片处理失败,请重试", Toast.LENGTH_SHORT).show();
    }
}

5. 权限与配置调整

  • 注意:上述流程不需要申请READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限,高版本系统会自动适配,只要保留相机权限即可正常调用系统相机
  • 原有的android:requestLegacyExternalStorage="true"配置可以保留,对Android 9及以下版本生效,Android11以上会自动忽略,不会造成兼容问题
  • 不要为了图省事申请MANAGE_EXTERNAL_STORAGE全局存储权限,这类权限会被应用商店严格审核,完全没必要——上述方案不需要任何特殊权限即可实现全流程选图、拍照、上传功能

6. Retrofit上传适配

因为最终用于上传的文件是应用私有目录下的缓存文件,应用本身有完全读写权限,直接把文件转成Retrofit要求的RequestBody即可,完全不需要额外的权限申请或Uri转换逻辑。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.05 04:06:07