Android11+卸载重装/清数据后访问自建目录遇EACCES权限拒绝
问题分析与解决方案
问题根源
Android 11(API 30)引入的Scoped Storage机制,彻底改变了应用访问公共存储的规则:
- 公共Documents目录属于系统管理的共享存储区域,应用通过文件路径直接创建的目录,其权限关联仅对当前应用实例有效。
- 当应用被清除数据或卸载重装后,系统会销毁原应用的权限上下文,即使包名、签名完全一致,新实例也无法通过原文件路径直接访问旧实例创建的目录——系统不再识别两者的归属关联,因此抛出
EACCES权限拒绝错误。
推荐解决方案:使用MediaStore API操作公共目录
Scoped Storage要求应用通过MediaStore API访问共享存储中的文件/目录,该方式会让系统记录资源的访问权限,即使应用重装,只要通过API查询到目标资源的Uri,就能正常读写。
1. 查询或创建GT-backup目录
private static Uri getBackupDirectoryUri(Context context) { // 查询GT-backup目录是否存在 String[] projection = {MediaStore.Files.FileColumns._ID}; String selection = MediaStore.Files.FileColumns.DISPLAY_NAME + " = ? AND " + MediaStore.Files.FileColumns.MIME_TYPE + " = ?"; String[] selectionArgs = {"GT-backup", "vnd.android.document/directory"}; Cursor cursor = context.getContentResolver().query( MediaStore.Files.getContentUri("external"), projection, selection, selectionArgs, null ); if (cursor != null && cursor.moveToFirst()) { long dirId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)); cursor.close(); // 返回已存在目录的Uri return MediaStore.Files.getContentUri("external").buildUpon() .appendPath(String.valueOf(dirId)) .build(); } else { // 目录不存在,创建新目录 ContentValues values = new ContentValues(); values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, "GT-backup"); values.put(MediaStore.Files.FileColumns.MIME_TYPE, "vnd.android.document/directory"); values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS); if (cursor != null) cursor.close(); return context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), values); } }
2. 在目录中创建/读写文件
通过上述方法获取目录Uri后,使用ContentResolver操作文件,无需直接访问文件路径:
// 创建备份文件 private static Uri createBackupFile(Context context, String fileName) throws IOException { Uri dirUri = getBackupDirectoryUri(context); if (dirUri == null) { throw new IOException("Failed to access backup directory"); } ContentValues fileValues = new ContentValues(); fileValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName); fileValues.put(MediaStore.Files.FileColumns.MIME_TYPE, "application/octet-stream"); // 根据文件类型调整 fileValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/GT-backup"); return context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), fileValues); } // 写入文件示例 public void writeBackupData(Context context, String fileName, byte[] data) { try { Uri fileUri = createBackupFile(context, fileName); try (OutputStream os = context.getContentResolver().openOutputStream(fileUri)) { os.write(data); } } catch (IOException e) { e.printStackTrace(); } } // 读取文件示例 public byte[] readBackupData(Context context, String fileName) throws IOException { // 先查询目标文件的Uri String[] projection = {MediaStore.Files.FileColumns._ID}; String selection = MediaStore.Files.FileColumns.DISPLAY_NAME + " = ? AND " + MediaStore.Files.FileColumns.RELATIVE_PATH + " = ?"; String[] selectionArgs = {fileName, Environment.DIRECTORY_DOCUMENTS + "/GT-backup"}; Cursor cursor = context.getContentResolver().query( MediaStore.Files.getContentUri("external"), projection, selection, selectionArgs, null ); if (cursor != null && cursor.moveToFirst()) { long fileId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)); cursor.close(); Uri fileUri = MediaStore.Files.getContentUri("external").buildUpon() .appendPath(String.valueOf(fileId)) .build(); try (InputStream is = context.getContentResolver().openInputStream(fileUri); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } } else { if (cursor != null) cursor.close(); throw new FileNotFoundException("Backup file not found"); } }
权限配置
在AndroidManifest.xml中声明必要权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <!-- Android 13+ 需要细分权限 --> <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" />
动态申请权限的逻辑与常规存储权限申请一致,此处不再赘述。
备选方案:使用MANAGE_EXTERNAL_STORAGE权限(不推荐)
若必须通过文件路径直接访问公共目录,可申请MANAGE_EXTERNAL_STORAGE权限,但该权限为全存储访问权限,Google Play会严格审核非必要使用该权限的应用,可能导致上架被拒。
配置与申请
- 在Manifest中声明:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:minSdkVersion="30" />
- 动态申请权限:
private static final int REQUEST_MANAGE_STORAGE = 1001; public void checkManageStoragePermission(Context context) { if (!Environment.isExternalStorageManager()) { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); Uri uri = Uri.fromParts("package", context.getPackageName(), null); intent.setData(uri); ((Activity) context).startActivityForResult(intent, REQUEST_MANAGE_STORAGE); } }
备选方案:使用应用专属外部存储
如果备份文件无需在卸载后保留,可将目录放在应用专属外部存储区,该区域无需额外权限,且清除数据后仍可访问:
File backupDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "GT-backup"); if (!backupDir.exists()) { backupDir.mkdirs(); } // 后续直接通过File类操作文件即可
注意:卸载应用时,该目录会被系统自动删除。
内容的提问来源于stack exchange,提问作者Amer Al zibak
相关产品推荐
相关产品推荐

