
本文详解如何在 android api 13 及以上(重点覆盖 android 11/r、12/s、13/t 及更高版本)正确申请存储权限,并将 imageview 中的图片安全保存至公共相册目录,兼顾兼容性、最佳实践与 mediastore 集成。
本文详解如何在 android api 13 及以上(重点覆盖 android 11/r、12/s、13/t 及更高版本)正确申请存储权限,并将 imageview 中的图片安全保存至公共相册目录,兼顾兼容性、最佳实践与 mediastore 集成。
在 Android 权限模型持续演进的背景下,尤其是从 Android 11(API 30)引入分区存储(Scoped Storage)起,传统 WRITE_EXTERNAL_STORAGE 权限已大幅受限;而 Android 13(API 33)进一步废弃了 MANAGE_EXTERNAL_STORAGE 权限(除非符合特定用例),并强制要求使用更精细的媒体权限。因此,单一权限声明 + 一次性授权已不可行,必须按目标 SDK 版本分层处理。
✅ 正确声明权限(AndroidManifest.xml)
根据最低支持 API 级别和功能需求,需在 AndroidManifest.xml 中声明以下权限(注意:MANAGE_EXTERNAL_STORAGE 仅限必要场景且需 Google Play 审核批准,生产环境应优先避免):
<!-- 基础读写权限(API < 30 必需) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<!-- Android 10+(API 29+)读取媒体文件 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- ⚠️ 仅当绝对需要访问所有文件(如文件管理器类应用)且已通过 Play 审核才启用 -->
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> -->? 提示:
android:maxSdkVersion="29"确保WRITE_EXTERNAL_STORAGE不在 Android 10+ 上被系统忽略或触发异常。
✅ 运行时权限请求(分版本适配)
Android 6.0(API 23)起需动态申请危险权限;Android 11(API 30)起需额外引导用户手动开启“所有文件访问权限”;Android 13(API 33)起则统一转向 READ_MEDIA_* 细粒度权限。
在 Activity 中定义两个 ActivityResultLauncher:
// 用于请求 READ_MEDIA_IMAGES(Android 13+ 推荐)
private ActivityResultLauncher<String> mediaPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
saveImageFromImageView(); // 授权成功后执行保存
} else {
Toast.makeText(this, "需允许访问照片以保存图片", Toast.LENGTH_SHORT).show();
}
});
// 用于 Android 11+ 手动跳转设置页(仅当需 MANAGE_EXTERNAL_STORAGE 时使用)
private ActivityResultLauncher<Intent> storageSettingsLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (isMediaPermissionGranted()) {
saveImageFromImageView();
} else {
Toast.makeText(this, "未授予存储访问权限", Toast.LENGTH_SHORT).show();
}
});配套权限检查与请求逻辑:
private boolean isMediaPermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
== PackageManager.PERMISSION_GRANTED;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11+
return Environment.isExternalStorageManager(); // 或改用 READ_MEDIA_*(推荐)
} else { // API < 30
return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
}
private void requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 推荐:优先尝试 READ_MEDIA_IMAGES(即使低于 TIRAMISU,部分 OEM 已支持)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mediaPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES);
} else {
// 回退到 Android 11 的设置页方案(不推荐新项目使用)
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
storageSettingsLauncher.launch(intent);
}
} else {
// API < 30:请求 WRITE_EXTERNAL_STORAGE(需 maxSdkVersion 限制)
mediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}✅ 安全保存 ImageView 图片(适配分区存储)
不再使用 Environment.getExternalStoragePublicDirectory()(已弃用),而是通过 MediaStore 直接插入相册,确保图片立即可见且符合隐私规范:
private void saveImageFromImageView() {
ImageView imageView = findViewById(R.id.imageView);
Bitmap bitmap = getBitmapFromImageView(imageView);
if (bitmap == null) return;
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_" + System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
// 插入 MediaStore(自动分配路径,无需手动创建文件)
Uri uri = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
);
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
Toast.makeText(this, "图片已保存至相册", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Toast.makeText(this, "保存失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}
private Bitmap getBitmapFromImageView(ImageView iv) {
Drawable drawable = iv.getDrawable();
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
// 若为 VectorDrawable 或其他类型,建议先渲染为 Bitmap
return null;
}⚠️ 关键注意事项
-
不要硬编码路径:
getExternalStoragePublicDirectory()在 Android 10+ 被标记为@Deprecated,且在 Android 11+ 受限,务必使用MediaStore。 -
权限最小化原则:Android 13+ 应仅申请
READ_MEDIA_IMAGES,而非MANAGE_EXTERNAL_STORAGE;后者需 Play Store 特殊审核,且自 2023 年起新应用基本无法获批。 -
Bitmap 内存管理:大图需采样压缩(如
BitmapFactory.Options.inSampleSize),避免 OOM。 -
异步执行:图片压缩与写入 I/O 应放在子线程(如
Executors.newSingleThreadExecutor()),避免阻塞主线程。 -
空值与状态校验:始终检查
imageView.getDrawable()是否为null,以及bitmap是否有效。
通过以上结构化适配,你的应用可在 Android 13+ 设备上稳定、合规地完成图片保存任务,同时保持对旧版本的良好兼容性。


















