You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 20:24:33