Android 10+如何通过URI将SharedPreferences从内部存储复制到外部存储
失效问题核心根因
- 根本原因是Android 10+引入的**分区存储(Scoped Storage)**机制:哪怕你已经声明了
MANAGE_EXTERNAL_STORAGE这类高危权限,不同厂商的ROM适配差异、版本间的权限规则收紧,都会导致传统File路径直接写公共外部存储的逻辑出现不可预期的失效,Android 13+的兼容性问题会更突出。 - 你现有写的
copyFile()方法本身没有任何逻辑问题,完全可以在所有场景复用,只需要针对不同的存储场景,替换掉输入流、输出流的获取方式即可。
方案1:应用专属外部目录备份(零权限,最高稳定性,优先推荐)
你现有copyAsset()方法里已经在用的getExternalFilesDir()路径,是完全不受分区存储限制的,不需要申请任何存储权限就能读写,不需要改动copyFile()逻辑,只需要把SharedPreferences的源文件流对接进来就行:
- SharedPreferences的物理存储路径是固定的:应用内部存储目录下的
/shared_prefs/文件夹,文件名就是你创建SP实例时传入的名称(比如默认命名格式是应用包名_preferences.xml) - 具体实现逻辑和你现有代码结构完全对齐:
// 备份SharedPreferences到应用专属外部目录 private void backupSPToPrivateExternal(String spFileName) { // 读取内部存储的SP源文件 File spSourceFile = new File(getApplicationInfo().dataDir + "/shared_prefs/" + spFileName); if (!spSourceFile.exists()) { Toast.makeText(this, "待备份的SP文件不存在", Toast.LENGTH_SHORT).show(); return; } // 目标目录和你copyAsset里用的路径保持一致即可 String targetDirPath = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/MyBackup"; File targetDir = new File(targetDirPath); if (!targetDir.exists()) { targetDir.mkdirs(); } InputStream in = null; OutputStream out = null; try { in = new FileInputStream(spSourceFile); File targetFile = new File(targetDirPath, "sp_backup_" + System.currentTimeMillis() + ".xml"); out = new FileOutputStream(targetFile); // 你现有写好的copyFile逻辑直接复用,无需任何修改 copyFile(in, out); Toast.makeText(this, "备份成功,路径:" + targetFile.getAbsolutePath(), Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "备份失败", Toast.LENGTH_SHORT).show(); } finally { // 流关闭逻辑和你现有代码完全一致 if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }
- 方案优势:全版本Android兼容,不需要动态申请存储权限,不需要引导用户跳转设置开启高危权限,完全符合系统存储规则。缺点是该目录的文件会在用户卸载应用时被自动清除,其他应用无法直接访问。
方案2:通过SAF(URI访问)备份到公共外部存储
如果需要把备份文件存到公共Documents目录,让用户能通过系统文件管理器直接访问查看,就不需要硬扛分区存储规则用传统File路径写,直接用系统自带的SAF(存储访问框架)拿到目标文件的URI,对接你现有的copyFile()逻辑即可:
- 触发系统文件选择器,让用户自主选择备份的保存位置,全程不需要申请任何存储权限:
// 启动SAF创建备份文件的入口 private void triggerSAFCreateBackup() { Intent createFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT); createFileIntent.addCategory(Intent.CATEGORY_OPENABLE); createFileIntent.setType("text/xml"); createFileIntent.putExtra(Intent.EXTRA_TITLE, "sp_backup_" + System.currentTimeMillis() + ".xml"); // 用户选完位置后,会通过Activity回调返回选中的文件URI startActivityForResult(createFileIntent, REQUEST_CODE_BACKUP); }
- 在Activity的回调里拿到选中的URI,通过ContentResolver打开输出流,复用现有复制逻辑:
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_BACKUP && resultCode == RESULT_OK) { if (data == null || data.getData() == null) return; Uri targetFileUri = data.getData(); // 读取内部存储的SP源文件 File spSourceFile = new File(getApplicationInfo().dataDir + "/shared_prefs/" + "你的SP文件名.xml"); InputStream in = null; OutputStream out = null; try { in = new FileInputStream(spSourceFile); // 核心适配点:通过URI拿到输出流,完全绕开传统File路径的权限限制 out = getContentResolver().openOutputStream(targetFileUri); // 现有copyFile逻辑原封不动复用 copyFile(in, out); Toast.makeText(this, "备份成功", Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "备份失败", Toast.LENGTH_SHORT).show(); e.printStackTrace(); } finally { // 流关闭逻辑和你现有代码完全一致 if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
- 恢复逻辑的思路和备份完全对称:通过SAF让用户选中之前备份的文件URI,通过ContentResolver打开输入流,将流内容覆盖写入到内部存储的SP文件路径,写完后清空SP缓存重新加载即可生效。
实际开发注意事项
MANAGE_EXTERNAL_STORAGE属于应用商店审核的高危权限,普通工具类应用申请该权限很容易被平台驳回,使用SAF方案完全不需要申请这类高危权限,符合平台政策要求。- 不建议继续使用
Environment.getExternalStoragePublicDirectory()这类传统公共路径API,在Android 10以上就算持有存储权限,不同厂商ROM也会出现路径不可写的问题,稳定性远低于SAF和应用专属目录。
内容的提问来源于stack exchange,提问作者Tacker
相关产品推荐
相关产品推荐

