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
相关产品推荐
相关产品推荐

