学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为3024字,预计阅读6分钟
前言
互联网Android APP开发其实很多都是Android端写UI,业务逻辑通过API回调展示数据,而我这边主要是硬件设备还要打交道,平时也要考虑网络不通的情况下单机的正常使用,所以所有的业务逻辑都是在程序中实现,数据的本地化要求也高,那就需要用到Sqlite数据库,所以这篇文章就专门来说说Sqlite数据库的备份和还原。
怎么实现Sqlite的数据库备份和还原?
A
其实实现Sqlite的备份和还原原理还是比较简单,就是将App中生成的Sqlite的数据库文件复制到存放区域,还原就是将复制的数据库文件拷回到程序指定的数据库目录即可。但这里有个比较关键的问题,就是存放的目录必须是自己指定的外部目录,如果是备份数据库文件还是在程序包下的目录,遇到安装升级签名不对,或是手工打开应用程序点击了清除数据,那本地的数据库以及备份的数据库文件也会全部清空,那后果可想而知。。。。
实现效果
1.本地三个数据库文件
2.SD卡目录下没有备份文件
3.点击备份数据库
4.SD卡目录下已经拷贝过来数据库了
5.删除原来databases目录的数据库
6.重新查询后什么也没显示
7.点击还原数据库后databases目录下的文件已经拷贝回来了
8.重新点击查询数据,可以看到显示的数据了
核心代码

微卡智享
备份和还原类(DbBackupUtil)
package com.vaccae.roomdemo
import android.annotation.SuppressLint
import android.content.Context
import android.os.Environment
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
/**
* 作者:Vaccae
* 邮箱:3657447@qq.com
* 创建时间:18:22
* 功能模块说明:
*/
class DbBackupUtil {
private var mContext: Context? = null
val path =
Environment.getExternalStorageDirectory().absolutePath + File.separator + "RoomBackup" + File.separator
private fun getInstance(context: Context) {
mContext ?: run {
synchronized(DbBackupUtil::class.java) {
mContext = context
}
}
}
private fun createPath() {
//安装包路径
val updateDir = File(path)
//创建文件夹
if (!updateDir.exists()) {
updateDir.mkdirs()
}
}
suspend fun backup(context: Context): Flow<String> = flow {
getInstance(context)
createPath()
mContext?.let {
val strs = it.databaseList()
emit("共${strs.size}个数据库文件,开始备份")
for (str in strs) {
emit("正在备份${str}数据库。。。")
//找到文件的路径 /data/data/包名/databases/数据库名称
val dbFile = it.getDatabasePath(str)
//val dstFile = it.getExternalFilesDir("db").toString() + "/" + str
val dstFile = path + str;
var fis: FileInputStream? = null
var fos: FileOutputStream? = null
try {
//文件复制到sd卡中
fis = FileInputStream(dbFile)
fos = FileOutputStream(dstFile)
var len = 0
val buffer = ByteArray(2048)
while (-1 != fis.read(buffer).also({ len = it })) {
fos.write(buffer, 0, len)
}
fos.flush()
emit("${str}数据库备份完成。。。")
} catch (e: Exception) {
throw e
} finally {
//关闭数据流
try {
fos?.close()
fis?.close()
} catch (e: IOException) {
throw e
}
}
}
emit("所有数据库备份完成")
} ?: kotlin.run { throw Exception("未定义Context") }
}
suspend fun restore(context: Context): Flow<String> {
return flow {
getInstance(context)
createPath()
mContext?.let {
//var dbfiles = it.getExternalFilesDir("db")
var dbfiles = File(path)
dbfiles.let { dbs ->
var files = dbs.listFiles()
if (files.isNotEmpty()) {
emit("共${files.size}个数据库文件,开始还原")
for (str in files) {
var dbFile = it.getDatabasePath(str.name)
dbFile.delete()
var fis: FileInputStream? = null
var fos: FileOutputStream? = null
try {
//文件复制到sd卡中
fis = FileInputStream(str)
fos = FileOutputStream(dbFile)
var len = 0
val buffer = ByteArray(2048)
while (-1 != fis.read(buffer).also({ len = it })) {
fos.write(buffer, 0, len)
}
fos.flush()
emit("${str}数据库还原完成。。。")
} catch (e: Exception) {
throw e
} finally {
//关闭数据流
try {
fos?.close()
fis?.close()
} catch (e: IOException) {
throw e
}
}
}
emit("所有数据库还原完成")
}
}
} ?: kotlin.run { throw Exception("未定义Context") }
}
}
}
Activity中的调用
//备份调用
btnbackup.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
DbBackupUtil().backup(applicationContext)
.flowOn(Dispatchers.IO)
.collect(collector = FlowCollector { t ->
tvshow.text = t
})
}
}
//还原调用
btnrestore.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
DbBackupUtil().restore(applicationContext)
.flowOn(Dispatchers.IO)
.collect(collector = FlowCollector { t ->
tvshow.text = t
})
}
}
微卡智享
重点说明
1.备份和还原采用返回flow的形式,因为数据库文有三个,这样可以在UI界面显示还原的进度。
发送当前进度
UI显示当前进度
2.备份的数据库文件存放到SD卡自定义目录中,防止应用程序点击清除数据后,备份文件如果也是拷贝到程序包目录下的也会一起删除。不过针对存储权限,在 Android 6.0 之后就变成了危险权限,而到了 Android 11 上面变成了特殊权限,必须加上授权所有文件管理权限才行。
AndroidManifest中加入清单权限
还有存放的目录
对应的xml/file_pahts.xml中定义external-path
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vaccae.roomdemo">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
file_path.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--最终设置里面NAME和PATH都要和文件夹的名称一样,才会找到对应的文件-->
<external-path name="RoomBackup" path="RoomBackup"/>
</paths>
Activity中动态申请权限
StartActivityforResult已经废弃了,所以改用registerForActivityResult来实现
申请权限函数
MainActivity申请权限代码
class MainActivity : AppCompatActivity() {
companion object {
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.CHANGE_NETWORK_STATE,
Manifest.permission.INTERNET
)
}
//StartActivity弃用后,使用registerForActivityResult来实现
private val requestDataLauncher =
registerForActivityResult(object : ActivityResultContract<Int, String>() {
override fun createIntent(context: Context, input: Int?): Intent {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:$packageName")
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): String {
TODO("Not yet implemented")
}
}
) {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
}
private fun allPermissionsGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先判断有没有权限
if (Environment.isExternalStorageManager()) {
REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext,
it
) == PackageManager.PERMISSION_GRANTED
}
} else {
requestDataLauncher.launch(REQUEST_CODE_PERMISSIONS)
}
} else {
REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext,
it
) == PackageManager.PERMISSION_GRANTED
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//申请权限
allPermissionsGranted()
}
}
这样整个Android本地数据库的备份和还原就完成了,Demo我是直接加在的使用原来做的远程查询分析小工具的那个Demo里《制作一个Android Sqlite远程运维小工具》,完整的源码链接在下方:
源码地址
https://github.com/Vaccae/TransAndroidSqliteDBDemo.git
点击阅读原文可以看到“码云”的地址
完
往期精彩回顾