
本文详解 android 11 及以上系统中因存储权限变更导致的 `open failed: eperm (operation not permitted)` 错误,提供适配 android 10–14 的兼容性写法、清单配置与代码优化方案。
在 Android 10(API 29)引入分区存储(Scoped Storage)后,应用对共享外部存储(如 /sdcard/)的直接写入能力被大幅限制。即使已动态申请 WRITE_EXTERNAL_STORAGE 等传统权限,调用 new FileOutputStream(new File("/storage/emulated/0/YourName.png")) 仍会抛出 EPERM (Operation not permitted) 异常——这并非权限未授予,而是系统强制执行的访问策略变更。
✅ 正确做法:按目标 SDK 版本分层适配
1. 优先使用应用专属目录(推荐,无需权限)
最安全、最兼容的方式是将截图保存至应用私有外部目录(getExternalFilesDir()),该路径无需任何运行时权限,且在卸载应用时自动清理:
public void takeAndSaveScreenshot(Activity activity) {
View rootView = activity.getWindow().getDecorView();
rootView.setDrawingCacheEnabled(true);
rootView.buildDrawingCache();
Bitmap fullBitmap = rootView.getDrawingCache();
// 获取可见区域(排除状态栏)
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
DisplayMetrics metrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
Bitmap screenshot = Bitmap.createBitmap(
fullBitmap, 0, statusBarHeight,
metrics.widthPixels, metrics.heightPixels - statusBarHeight
);
rootView.destroyDrawingCache();
try {
// ✅ 安全路径:应用专属目录(无需权限,Android 4.4+ 兼容)
File dir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (dir == null) throw new IOException("External files dir unavailable");
File file = new File(dir, "Screenshot_" + System.currentTimeMillis() + ".png");
try (FileOutputStream out = new FileOutputStream(file)) {
screenshot.compress(Bitmap.CompressFormat.PNG, 100, out);
}
// 可选:通知媒体扫描器(仅对公共目录生效,私有目录无需此步)
// MediaScannerConnection.scanFile(activity, new String[]{file.getAbsolutePath()}, null, null);
Toast.makeText(activity, "已保存至:" + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(activity, "保存失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
}2. 若必须保存到公共目录(如 Documents/Pictures)
需根据 Android 版本选择不同策略:
-
Android 10(API 29):
在 AndroidManifest.xml 的 <application> 标签中添加:android:requestLegacyExternalStorage="true"
并申请 WRITE_EXTERNAL_STORAGE(仍需动态请求)。
-
Android 11(API 30):
requestLegacyExternalStorage 失效,必须使用 MediaStore API 或 MANAGE_EXTERNAL_STORAGE(仅限特定场景,需 Google Play 审核)。
推荐通过 MediaStore.Images.Media.EXTERNAL_CONTENT_URI 插入图片:ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, "Screenshot_" + System.currentTimeMillis() + ".png"); values.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp"); ContentResolver resolver = activity.getContentResolver(); Uri insertUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (insertUri != null) { try (OutputStream out = resolver.openOutputStream(insertUri)) { screenshot.compress(Bitmap.CompressFormat.PNG, 100, out); } } Android 13+(API 33):
MANAGE_EXTERNAL_STORAGE 权限被完全移除,仅支持 MediaStore 或 Storage Access Framework (SAF)。此时应放弃直接文件路径操作,改用 SAF 由用户选择保存位置(适用于导出等强需求场景)。
⚠️ 重要注意事项
- ❌ 不要再使用 Environment.getExternalStorageDirectory() —— 该路径在 Android 10+ 上不可写(即使有权限);
- ❌ WRITE_EXTERNAL_STORAGE 在 targetSdk >= 30 时对公共目录无效;
- ✅ getExternalFilesDir() 和 getCacheDir() 始终可用,是首选;
- ✅ 使用 try-with-resources 自动关闭流,避免资源泄漏;
- ✅ 截图前确保 Activity 已完成绘制(建议在 onResume() 或点击回调中调用,避免空 Bitmap);
- ? 调试时可通过 Log.d("Path", file.getAbsolutePath()) 验证路径是否真实可写。
总结
EPERM 错误本质是 Android 存储模型演进的必然结果。开发者应主动拥抱分区存储:
? 日常截图/缓存 → 用 getExternalFilesDir()(零权限、高兼容);
? 用户导出需求 → 用 MediaStore 或 SAF(符合平台规范);
? 彻底弃用 Environment.getExternalStorageDirectory() 直接写入逻辑。
适配不是妥协,而是构建更安全、更可持续的 Android 应用的必经之路。










