
本文详解在 Android 11 及更高版本中,如何绕过已废弃的 Environment.getExternalStoragePublicDirectory() 和失效的 DownloadManager,使用作用域存储(Scoped Storage)安全、可靠地下载网络图片,并适配运行时权限与文件路径变更。
本文详解在 android 11 及更高版本中,如何绕过已废弃的 `environment.getexternalstoragepublicdirectory()` 和失效的 `downloadmanager`,使用作用域存储(scoped storage)安全、可靠地下载网络图片,并适配运行时权限与文件路径变更。
在 Android 11(API 30)及以上版本中,Google 强制启用分区存储(Scoped Storage),导致传统基于 Environment.getExternalStoragePublicDirectory() 的外部存储写入方式被弃用,DownloadManager 也因目标 URI 权限限制和 setDestinationUri() 不兼容新规则而常静默失败——这正是你遇到“无报错、无通知、无文件”的根本原因。
✅ 正确方案:使用 MediaStore 写入公共媒体目录(推荐)
自 Android 10(API 29)起,MediaStore.Images.Media.INSERTION_URI 是向 DCIM/ 或 Pictures/ 等公共目录安全写入图片的标准方式,无需 WRITE_EXTERNAL_STORAGE 权限,且完全兼容 Android 11+。
以下是经过实测、符合现代 Android 最佳实践的完整下载方法:
private void downloadImage(String url, String displayName) {
// 1. 构建 MediaStore 插入内容值
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
// 2. 获取输出流(自动创建文件并返回 OutputStream)
Uri uri;
try (ContentResolver resolver = getContentResolver();
OutputStream os = resolver.openOutputStream(
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values))) {
if (os == null) {
Log.e("Download", "Failed to open output stream for MediaStore");
return;
}
// 3. 下载并写入
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(15_000);
conn.setReadTimeout(15_000);
int responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP " + responseCode + " for " + url);
}
try (InputStream is = conn.getInputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
}
Log.i("Download", "✅ Saved to MediaStore: " + displayName);
} catch (Exception e) {
Log.e("Download", "❌ Download failed", e);
Toast.makeText(this, "Download failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
调用示例:
normal.setOnClickListener(v ->
downloadImage("https://apod.nasa.gov/apod/image/0611/ngc6745_hst.jpg", "nasa_apod_200611.jpg")
);
⚠️ 关键注意事项
-
无需
WRITE_EXTERNAL_STORAGE权限:MediaStore写入公共媒体目录属于“豁免场景”,仅需READ_MEDIA_IMAGES(仅读取时才需);但若应用目标 SDK ≥ 33(Android 14),需声明android.permission.READ_MEDIA_IMAGES(即使只写入)——请在AndroidManifest.xml中添加:<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"></uses-permission>
-
DownloadManager失效原因:setDestinationUri()在 Android 11+ 上对非应用专属目录(如Environment.DIRECTORY_DOWNLOADS)会静默拒绝;且getExternalFilesDir()返回的是应用私有目录,无法被系统相册或文件管理器识别。 -
getExternalStoragePublicDirectory()已废弃:自 API 29 起标记为@Deprecated,Android 11+ 默认禁用,强行使用将触发SecurityException(即使声明了权限)。 -
网络请求必须在后台线程执行:上述代码应在
AsyncTask(已弃用)、Executors或Coroutine中调用,避免NetworkOnMainThreadException。例如使用Executors.newSingleThreadExecutor()包裹。
✅ 补充:如需下载到应用私有目录(不共享)
若仅需保存供本应用内部使用(如缓存),应使用 getCacheDir() 或 getExternalCacheDir(),无需任何权限,且路径稳定:
File cacheFile = new File(getExternalCacheDir(), "temp_image.jpg"); // 后续用 FileOutputStream 写入即可(注意:此文件不会出现在相册中)
总结
| 方式 | 兼容性 | 是否需权限 | 是否可见于相册 | 推荐度 |
|---|---|---|---|---|
MediaStore(本文方案) |
✅ Android 10+ | ❌(仅需 READ_MEDIA_IMAGES for SDK≥33) |
✅ | ⭐⭐⭐⭐⭐ |
DownloadManager |
❌ Android 11+ 易失败 | ✅(WRITE_EXTERNAL_STORAGE 无效) |
✅ | ⚠️ 不推荐 |
getExternalStoragePublicDirectory() |
❌ 已废弃 | ✅(但无效) | ✅(历史遗留) | ❌ 禁用 |
遵循 MediaStore 方案,你不仅能解决当前下载问题,更可确保应用长期兼容未来 Android 版本。务必在 targetSdkVersion ≥ 30 的 build.gradle 中启用 android:requestLegacyExternalStorage="false"(默认即关闭),真正拥抱现代存储模型。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











