Android 11及以上版本应用SQLite数据库复制到外部存储的解决方法
Android 11+ 数据库导出到外部存储失败的解决方案
问题根源
Android 11(API 30)及以上版本启用了分区存储(Scoped Storage),直接通过File API写入外部存储的任意路径会被系统限制;同时原代码中AsyncTask已被废弃,且数据库文件路径获取逻辑冗余,这些都是导致报错operation not allowed的核心原因。
解决方案步骤
1. 适配分区存储规则
推荐两种适配方案:
- 方案A:应用专属外部存储:无需额外权限,文件仅本应用可访问,适合不需要用户在文件管理器中直接查看的场景,路径为
getExternalFilesDir(null)。 - 方案B:存储访问框架(SAF):通过系统文件选择器让用户主动选择保存目录,避免权限限制,适合需要用户自定义保存路径的场景(下文以该方案为例)。
2. 修正数据库文件获取方式
使用系统提供的Context.getDatabasePath()方法获取私有数据库文件,比手动拼接路径更可靠:
File dbFile = getDatabasePath("MOC.db");
3. 替换过时的AsyncTask
改用Coroutine(推荐)或ExecutorService处理后台任务,避免使用已废弃的API。
完整修改代码示例(SAF方案)
第一步:启动SAF选择保存目录
private static final int REQUEST_CODE_SAVE_DIR = 1001; // 触发导出流程 private void startDatabaseExport() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_SAVE_DIR); } // 处理SAF选择结果 @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_SAVE_DIR && resultCode == RESULT_OK && data != null) { Uri treeUri = data.getData(); // 持久化目录权限,避免重启应用后失效 getContentResolver().takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION ); // 启动导出任务 exportDatabaseToSelectedDir(treeUri); } }
第二步:数据库导出逻辑(使用Coroutine)
private void exportDatabaseToSelectedDir(Uri treeUri) { ProgressDialog dialog = ProgressDialog.show(this, "", "Exporting database..."); CoroutineScope(Dispatchers.IO).launch { boolean exportSuccess = false; try { File dbFile = getDatabasePath("MOC.db"); if (!dbFile.exists()) { Log.e("mypck", "Target database file not found"); notifyExportResult(dialog, false, "Database not found"); return@launch; } // 生成带时间戳的文件名(修正原代码中年份格式错误:yyy → yyyy) SimpleDateFormat dateFormat = new SimpleDateFormat( "dd-MM-yyyy_hh:mm:ss", Locale.getDefault() ); String timestamp = dateFormat.format(new Date()); String outputFileName = "SMRD_Rishi_" + timestamp + ".db"; // 通过SAF创建输出文件 DocumentFile rootDir = DocumentFile.fromTreeUri(this, treeUri); if (rootDir == null || !rootDir.exists()) { Log.e("mypck", "Selected directory is invalid"); notifyExportResult(dialog, false, "Invalid save directory"); return@launch; } DocumentFile outputFile = rootDir.createFile( "application/x-sqlite3", outputFileName ); if (outputFile == null) { Log.e("mypck", "Failed to create output file"); notifyExportResult(dialog, false, "Failed to create file"); return@launch; } // 复制数据库文件(使用try-with-resources自动关闭流) try (InputStream inputStream = new FileInputStream(dbFile); OutputStream outputStream = getContentResolver().openOutputStream(outputFile.getUri())) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } exportSuccess = true; } } catch (IOException e) { Log.e("mypck", "Export error: " + e.getMessage(), e); } finally { notifyExportResult(dialog, exportSuccess, exportSuccess ? "Export successful!" : "Export failed"); } } } // 更新UI线程的导出结果 private void notifyExportResult(ProgressDialog dialog, boolean success, String message) { runOnUiThread(() -> { dialog.dismiss(); Toast.makeText(MainDashBoard.this, message, Toast.LENGTH_SHORT).show(); }); }
额外注意事项
- 权限说明:使用SAF或应用专属外部存储时,无需申请
WRITE_EXTERNAL_STORAGE权限,符合Android 11+的权限规范。 - 资源泄漏:改用
try-with-resources语法自动关闭流,避免手动关闭遗漏导致的资源泄漏问题。
内容的提问来源于stack exchange,提问作者TPCODL ODISHA
相关产品推荐
相关产品推荐

