Android 12 相册选图存私有目录无需READ_EXTERNAL_STORAGE权限疑问
核心原因
你不需要申请READ_EXTERNAL_STORAGE就能完成选图+存私有目录的逻辑,是两个机制共同作用的结果:
- 系统选择器的临时URI授权:你用的
ACTION_GET_CONTENT是调用系统级的文件选择组件,用户选中图片后,系统会自动给你的应用授予这张选中图片的临时读写权限,你通过返回的Uri访问这张图不需要任何全局存储权限,这个临时权限会在你应用退出、或者Uri被回收后自动失效。 - 应用私有目录无权限要求:你调用
saveToInternalStorage写入的是应用自身的私有目录(通过Context.getDir()获取的目录属于应用沙箱内部),写入这个目录从Android第一个版本开始就不需要申请任何存储权限,只有你的应用自己能访问这个目录下的文件。
旧版存储API与新版Photo Picker的权限规则差异
旧版全量存储权限模型(Android 10及以前,未启用分区存储)
- 如果要直接遍历、读取整个外部存储的所有文件(包括全相册图片),必须申请
READ_EXTERNAL_STORAGE(Android 6.0+是动态权限),授权后应用可以无限制访问外部存储所有内容,隐私风险极高。 - 即便是旧版本,使用
ACTION_GET_CONTENT/ACTION_PICK唤起系统选择器选单文件时,从Android 4.4开始就支持临时URI授权,哪怕没有申请全量存储权限,也可以正常获取用户选中的单个文件,只是早期很多开发者为了方便直接申请全量权限,绕过系统选择器直接扫描全量媒体库。
过渡阶段分区存储模型(Android 11、Android 12)
- 应用私有目录读写完全不需要权限。
- 如果要批量读取全相册、后台访问媒体文件,需要申请
READ_EXTERNAL_STORAGE,但授权后也只能访问共享存储里的图片、视频、音频三类媒体文件,无法访问下载目录下的非媒体文件、其他应用的私有目录文件。 - 继续使用
ACTION_GET_CONTENT选图时,依然走临时URI授权逻辑,不需要申请存储权限。
新版Photo Picker(Android 13正式提供,可通过AndroidX库兼容到Android 11)
- 完全不需要申请任何存储权限,是官方专门为媒体选择场景设计的组件。
- 权限粒度极细:用户选中哪些图片/视频,系统才会给应用颁发对应资源的临时访问权限,应用完全无法访问用户未选中的其他媒体内容,隐私保护能力远强于旧的全量权限模型。
- 不需要额外做权限申请、权限校验逻辑,API返回的Uri格式统一,兼容性比旧的
ACTION_GET_CONTENT更好,不会出现不同厂商选择器返回Uri格式不统一、读流失败的问题。
对你现有代码的优化提示
你现在写的权限申请逻辑在targetSdkVersion 31+、运行在Android 12及以上设备时是冗余的——因为你的选图流程根本不需要READ_EXTERNAL_STORAGE权限。另外有两个需要注意的坑:
- 你代码里定义了
filePathColumn去查MediaStore.Images.Media.DATA字段,这个字段在分区存储模型下很多场景拿不到有效路径,不要依赖这个字段做文件读取,你现在直接通过返回的Uri用ContentResolver获取Bitmap、或者打开输入流的方式是正确的。 - 你保存文件时文件名后缀是
.jpg,但压缩用的格式是Bitmap.CompressFormat.PNG,会导致文件后缀和实际格式不匹配,建议统一后缀和压缩格式。
修正问题点后的核心实现参考:
// 选图按钮点击逻辑,不需要提前申请存储权限 single.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(Intent.createChooser(intent,"Select Picture"), SINGLE_PICTURE); }); @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != Activity.RESULT_OK) { Snackbar.make(findViewById(android.R.id.content), "failure", Snackbar.LENGTH_LONG).show(); return; } switch (requestCode) { case SINGLE_PICTURE: if (data.getData() != null) { Uri mImageUri = data.getData(); try { Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), mImageUri); saveToInternalStorage(bitmap); } catch (IOException e) { e.printStackTrace(); } } break; } } private String saveToInternalStorage(Bitmap bitmapImage){ ContextWrapper cw = new ContextWrapper(getApplicationContext()); File directory = cw.getDir("imageDir", Context.MODE_PRIVATE); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); // 统一用png后缀,和压缩格式匹配 File mypath=new File(directory,timeStamp+".png"); try (FileOutputStream fos = new FileOutputStream(mypath)){ bitmapImage.compress(Bitmap.CompressFormat.PNG, 100, fos); } catch (Exception e) { e.printStackTrace(); } return directory.getAbsolutePath(); }
如果你的需求只是让用户选择图片/视频后做上传、保存到私有目录处理,不管是用旧的
ACTION_GET_CONTENT还是新的Photo Picker,都不需要申请任何存储权限;只有当你需要批量扫描全量媒体库、在后台不需要用户主动选文件就要访问媒体内容的时候,才需要申请对应版本的存储权限。
内容的提问来源于stack exchange,提问作者FriendlyGamer007
相关产品推荐
相关产品推荐

