Android下载文件报open failed: EPERM权限异常排查
Android URL下载文件报EPERM权限异常修复
问题复现场景
实现网络URL下载文件到本地存储功能,已动态申请存储权限且系统提示授权成功,目标保存路径为/storage/emulated/0/Movies/RoveDashCam/Photos/2022_0103_124619_001.JPG,运行时抛出如下异常:
java.io.FileNotFoundException: /storage/emulated/0/Movies/RoveDashCam/Photos/2022_0103_124619_001.JPG: open failed: EPERM (Operation not permitted)
原核心下载实现代码:
private String downloadFile(String MY_URL) { int count; try { URL url = new URL(MY_URL); URLConnection connection = url.openConnection(); connection.connect(); long startTime = System.currentTimeMillis(); int lengthOfFile = connection.getContentLength(); InputStream input = new BufferedInputStream(url.openStream(), 8192); fileName = MY_URL.substring(MY_URL.lastIndexOf('/') + 1, MY_URL.length()); File outputFile = new File(directory, fileName); OutputStream output = new FileOutputStream(outputFile); byte data[] = new byte[1024]; long total = 0; while ((count = input.read(data)) != -1) { total += count; int status = (int) ((total * 100) / lengthOfFile); output.write(data, 0, count); handler.post(new Runnable() { @Override public void run() { progress_circular_id.setProgress(status); textview_progress_status_id.setText(String.valueOf(status) + "%"); if (status == 100) { dialog.dismiss(); } } }); } output.flush(); output.close(); input.close(); return "Downloaded at: " + directory.getPath() + fileName; } catch (IOException e) { Log.e("Abhan", "Error: " + e.getLocalizedMessage()); runOnUiThread(new Runnable() { public void run() { hideProgressDialog(); } }); } hideProgressDialog(); return "Downloading Error"; }
问题根因
- Android 10(API 29)及以上版本默认启用分区存储机制,应用即使获取了
READ/WRITE_EXTERNAL_STORAGE权限,也无法直接通过File类随意访问外部存储公共目录下的自定义路径,直接创建FileOutputStream写入会被系统拦截抛出EPERM异常。 - 原代码未做目标父目录存在性校验,若
/storage/emulated/0/Movies/RoveDashCam/Photos/目录未提前创建,也会触发文件写入失败。 - 若应用
targetSdkVersion≥33(Android 13),WRITE_EXTERNAL_STORAGE权限已被系统废弃,即使动态申请也不会获得授权,直接访问文件路径的旧逻辑完全失效。 - 原代码中更新UI使用的
status变量为方法内局部变量,直接在匿名Runnable中访问会触发Java编译错误,需要做可访问性处理。
修复方案
推荐方案:使用MediaStore API写入公共目录(全版本兼容)
不要硬编码外部存储绝对路径,通过系统提供的MediaStore接口创建媒体文件,可直接指定保存到Movies公共目录下的自定义子目录,Android 10及以上版本写入应用自身创建的媒体文件无需申请任何存储权限,系统会自动创建对应父目录。
核心实现代码:
// 先在子线程执行下载逻辑 private String downloadFile(String fileUrl) { int count; InputStream input = null; OutputStream output = null; try { URL url = new URL(fileUrl); URLConnection connection = url.openConnection(); connection.connect(); int lengthOfFile = connection.getContentLength(); input = new BufferedInputStream(url.openStream(), 8192); String fileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1); // 构造图片文件的ContentValues ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); // 指定保存到Movies目录下的自定义子目录,系统自动创建 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/RoveDashCam/Photos"); } else { // Android 9及以下旧版本兼容,提前创建目录 File oldDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), "RoveDashCam/Photos"); if (!oldDir.exists()) oldDir.mkdirs(); File oldFile = new File(oldDir, fileName); output = new FileOutputStream(oldFile); } Uri imageUri = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (imageUri != null) { output = getContentResolver().openOutputStream(imageUri); } } if (output == null) return "Downloading Error"; byte[] data = new byte[1024]; long total = 0; while ((count = input.read(data)) != -1) { total += count; // 用final修饰进度值,解决匿名内部类访问局部变量的限制 final int status = (int) ((total * 100) / lengthOfFile); output.write(data, 0, count); handler.post(new Runnable() { @Override public void run() { progress_circular_id.setProgress(status); textview_progress_status_id.setText(status + "%"); if (status == 100) { dialog.dismiss(); } } }); } output.flush(); return "Download success"; } catch (IOException e) { Log.e("Abhan", "Error: " + e.getLocalizedMessage()); runOnUiThread(() -> hideProgressDialog()); return "Downloading Error"; } finally { // 统一在finally关流,避免异常时流泄漏 try { if (output != null) output.close(); if (input != null) input.close(); } catch (IOException e) { e.printStackTrace(); } } }
临时过渡方案(不推荐)
如果暂时不想重构代码,可以在AndroidManifest.xml的<application>标签下添加配置:
android:requestLegacyExternalStorage="true"
该配置仅在Android 10系统上生效,会临时关闭分区存储允许直接用File类访问路径,Android 11及以上版本系统会自动忽略该配置,无法解决高版本系统的权限问题,仅适合临时兼容使用。
权限配置注意事项
- 若应用
targetSdkVersion≥33,删除清单文件中WRITE_EXTERNAL_STORAGE权限声明,若需要读取其他应用创建的图片/视频文件,按需申请READ_MEDIA_IMAGES、READ_MEDIA_VIDEO权限即可。 - Android 9及以下版本,仍然需要动态申请
WRITE_EXTERNAL_STORAGE权限才能写入公共存储目录。
内容的提问来源于stack exchange,提问作者MARSH
相关产品推荐
相关产品推荐

