最近在写一个自用的轻量级记录 App,效果图如下:
所有的笔记和图片数据都存在手机里,因为是自己用,所以不会有服务器,但是自己辛辛苦苦记录的数据还是需要有一份保险,那就需要一个数据的本地备份和还原功能。
需求
整理需求如下:
- 可以把所有的笔记数据(包含图片等附件)都备份到手机sd卡上。
- 可以将备份的数据进加密。
- 其他的手机通过压缩包把数据进行还原,数据格式不能变。
由于使用的是 Jetpact Room 数据库,其实就是 Room 数据库的备份和还原,结合查看了网上的资料,总结出2种方案。
存储路径
在最新的 Android 13 上,app 的私有存储路径如下:
/data/data/com.ldlywt.note/
/sdcard/Android/data/com.ldlywt.note/
打开 Android Studio 查看对应的目录结构。
针对 /data/data/com.ldlywt.note/
这个目录是app的私有目录,不在sd卡上,使用手机文件管理器是查看不到的
从上图可知,room数据库在 database 文件夹目录下,图片和附件之类的存在 files 目录下,其他的可以不需要管。
针对 /sdcard/Android/data/com.ldlywt.note/
这个路径是sd卡的根目录,使用手机上的文件管理器可以查看到
第一个目录是放一些缓存,图片资源放在第二个 files 文件夹下面。
Json 导出
由于kotin的强大,将room数据库里面的内容导出为Json非常的容易。
kotlin
fun exportJson(context: Context, uri: Uri): Result<Unit> {
val json = Json.encodeToString(tagNoteRepo.queryAllNoteShowBeanList().toSet())
return runCatching {
BufferedOutputStream(context.contentResolver.openOutputStream(uri)).use { out: BufferedOutputStream ->
out.write(json.toByteArray())
}
}
}
如上所示,几行代码就可以把 Room 数据库的数据转换成 Json,然后输出到sd卡上。
导出格式如下:
处理 Json 数据就很容易了,但是有一个问题,attachments附件字段里面的path是绝对路径,换了手机后这里肯定不能写死,所以大量的图片数据要单独处理才行。
第一种方案
第一种方案的思路如下:
- 将 Room 数据库里面的数据导出为 Json
- 图片数据单独特殊处理
- 将 Json 数据和图片打包成zip压缩包
图片单独处理
每一个图片的名字是唯一的,那是不是把 attachments 里面的 path 直接替换成 filename 文件名字就行了,然后恢复的时候再将 filename 转变成 手机上的path 路径即可。
这是转换后的 json 数据
这里的具体代码有点多,见国外大佬的 GitHub:
但是我在实际使用中,这种方案存在一个问题:会导致room数据关系链的断裂。
因为我的app的表结构是:一条笔记Note对应多个Tag标签,一个Tag也会对应多条笔记,是多对多的关系。
kotlin
@Serializable
@Parcelize
@Entity(
primaryKeys = ["note_id", "tag"],
foreignKeys = [ForeignKey(
entity = Note::class,
parentColumns = arrayOf("note_id"),
childColumns = arrayOf("note_id"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
), ForeignKey(
entity = Tag::class,
parentColumns = arrayOf("tag"),
childColumns = arrayOf("tag"),
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class NoteTagCrossRef(
@ColumnInfo(name = "note_id") val noteId: Long, @ColumnInfo(index = true) val tag: String
) : Parcelable
上面方法将数据恢复到另外的手机后,会导致 Note 和 Tag 对应不上。
第二种方案
第二种方案我叫它整体搬迁方案,就是把需要的目录都整体打包成一个压缩包,还原时,再把以前app对应的目录删了,替换成备份的目录,然后重新启动app。
我的app需要的备份的是上面的三个目录。
将上面三个目录转变成list数据对象:
kotlin
suspend fun export(context: Context, uri: Uri): String = suspendCoroutine { continuation ->
context.contentResolver.openOutputStream(uri)?.use { stream ->
val out = ZipOutputStream(stream)
try {
val files = listOf(
ExportItem("/", File(context.dataDir.path + "/databases")),
ExportItem("/", context.filesDir),
ExportItem("/external/", context.getExternalFilesDir(null)!!)
)
for (i in files.indices) {
val item = files[i]
appendFile(out, item.dir, item.file)
}
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val fileName = cursor.getStringValue(OpenableColumns.DISPLAY_NAME)
continuation.resume(fileName)
}
}
} catch (e: Exception) {
continuation.resumeWithException(e)
} finally {
IOUtils.closeQuietly(out)
}
}
}
然后将其打包成zip压缩包到手机sdk上
kotlin
private fun appendFile(out: ZipOutputStream, dir: String, file: File) {
if (file.isDirectory) {
val files = file.listFiles() ?: return
for (childFile in files) {
appendFile(out, "$dir${file.name}/", childFile)
}
} else {
val entry = ZipEntry("$dir${file.name}")
entry.size = file.length()
entry.time = file.lastModified()
out.putNextEntry(entry)
FileInputStream(file).use { input ->
input.copyTo(out)
}
out.closeEntry()
}
}
导出的备份文件解压如下:
我现在用的就是这种方案,这种方案相对于第一种方案的对比如下:
- 代码量更少,实现简单
- 不要要转换path,容错率高
- 整体的替换,不能保存以前的数据
其实 Room 数据库的备份其实挺简单的,但是网上的资料东一块西一块,这里做个简单的总结。
Github
由于我写的碎碎记 App 已经被我上传到 Google Play,这里附上我开源练手的 Compose 项目地址:
不熟悉Compose代码也没关系,直接看备份的代码:BackUp.kt
后面会慢慢补上Room 数据库还原的代码~