Android开发:如何校验用户选择的.db数据库文件是否合法有效
SQLite数据库文件有效性校验实现方案
1. 优化文件选择器,降低用户选错概率
首先修改文件选择逻辑,限制可选文件的MIME类型,优先筛选SQLite数据库文件,减少用户误选其他文件的可能:
private fun selectDbFile() { val selectIntent = Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) // 优先匹配SQLite数据库的MIME类型,添加兼容类型适配不同文件管理器 type = "application/x-sqlite3" putExtra(Intent.EXTRA_MIME_TYPES, arrayOf( "application/x-sqlite3", "application/octet-stream", "application/sqlite" )) } val intent = Intent.createChooser(selectIntent, "选择数据库文件") intentLauncher.launch(intent) }
2. 多层校验逻辑实现(核心修改)
校验逻辑遵循「先校验再替换」的原则,全程不直接修改原数据库文件,避免校验失败导致原数据库损坏,共分三层校验:
- 第一层:校验SQLite文件固定魔数
- 第二层:校验文件是否可以正常打开为SQLite数据库
- 第三层:校验数据库表结构与应用要求匹配
3. 修改后的替换逻辑完整实现
fun replaceCurrentDb(context: Context, newDbUri: Uri?, completion: () -> Unit) { newDbUri ?: return CoroutineScope(Dispatchers.IO).launch { try { val contentResolver = context.contentResolver val src = contentResolver.openInputStream(newDbUri) ?: throw IOException("无法读取选中的文件") // 第一层校验:检查SQLite文件头魔数(前16字节固定为"SQLite format 3\0") val magicHeader = ByteArray(16) val readCount = src.read(magicHeader) val sqliteStandardHeader = "SQLite format 3\u0000".toByteArray() if (readCount < 16 || !magicHeader.contentEquals(sqliteStandardHeader)) { withContext(Dispatchers.Main) { Toast.makeText(context, "选中的文件不是有效的SQLite数据库文件", Toast.LENGTH_LONG).show() } src.close() return@launch } // 把选中的文件复制到缓存目录的临时文件,用于后续校验,不直接修改原数据库 val tempDbFile = File(context.cacheDir, "temp_verify_db.db") if (tempDbFile.exists()) tempDbFile.delete() val tempOutStream = FileOutputStream(tempDbFile) src.copyTo(tempOutStream) src.close() tempOutStream.close() // 第二层校验:尝试打开数据库,检查是否损坏 var tempDb: SQLiteDatabase? = null try { tempDb = SQLiteDatabase.openDatabase(tempDbFile.absolutePath, null, SQLiteDatabase.OPEN_READONLY) } catch (e: SQLiteException) { withContext(Dispatchers.Main) { Toast.makeText(context, "数据库文件已损坏,无法打开", Toast.LENGTH_LONG).show() } tempDbFile.delete() return@launch } // 第三层校验:检查应用需要的表是否都存在,确保结构匹配 // 把你应用里必须的表名替换到下面的数组里 val requiredTables = arrayOf("table1", "table2", "table3") var allTablesExist = true for (tableName in requiredTables) { val cursor = tempDb.rawQuery( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", arrayOf(tableName) ) cursor.moveToFirst() val count = cursor.getInt(0) cursor.close() if (count == 0) { allTablesExist = false break } } tempDb.close() if (!allTablesExist) { withContext(Dispatchers.Main) { Toast.makeText(context, "数据库文件结构与应用不匹配", Toast.LENGTH_LONG).show() } tempDbFile.delete() return@launch } // 所有校验通过,替换原数据库文件 val originalDbFile = context.getDatabasePath(MenuSQLiteHelper.DATABASE_NAME) // 先关闭原数据库连接,避免替换失败 MenuSQLiteHelper.getInstance(context).close() tempDbFile.copyTo(originalDbFile, overwrite = true) tempDbFile.delete() // 回到主线程回调成功 withContext(Dispatchers.Main) { completion() } } catch (e: IOException) { withContext(Dispatchers.Main) { Toast.makeText(context, "操作失败:${e.message}", Toast.LENGTH_LONG).show() } Log.e("DB_REPLACE", "替换数据库异常", e) } } }
额外注意事项
- 替换数据库成功后,需要重新初始化你的数据库实例,避免应用仍使用旧的缓存连接报错
- 不推荐使用GlobalScope,建议根据实际场景替换为
viewModelScope或者lifecycleScope,避免内存泄漏
内容的提问来源于stack exchange,提问作者Badr Bujbara
相关产品推荐
相关产品推荐

