优化
This commit is contained in:
@@ -17,7 +17,9 @@
|
||||
package com.sw.dualscreen.objbox
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import com.sw.plate.App
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.BoxStoreBuilder
|
||||
@@ -26,12 +28,22 @@ import io.objectbox.android.ObjectBoxLiveData
|
||||
import io.objectbox.config.DebugFlags
|
||||
import io.objectbox.exception.DbException
|
||||
import io.objectbox.exception.FileCorruptException
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import io.objectbox.query.Query
|
||||
import io.objectbox.sync.Sync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlin.also
|
||||
import kotlin.and
|
||||
import kotlin.io.copyTo
|
||||
import kotlin.io.inputStream
|
||||
import kotlin.io.outputStream
|
||||
@@ -52,8 +64,7 @@ object ObjectBox {
|
||||
|
||||
private const val TAG = "ObjectBox"
|
||||
|
||||
lateinit var boxStore: BoxStore
|
||||
private set
|
||||
var boxStore: BoxStore? = null
|
||||
|
||||
/**
|
||||
* If building the [boxStore] failed, contains the thrown error message.
|
||||
@@ -65,9 +76,9 @@ object ObjectBox {
|
||||
// On Android make sure to pass a Context when building the Store.
|
||||
boxStore = try {
|
||||
MyObjectBox.builder()
|
||||
.androidContext(context.applicationContext)
|
||||
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
|
||||
.build()
|
||||
.androidContext(context.applicationContext)
|
||||
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
|
||||
.build()
|
||||
} catch (e: DbException) {
|
||||
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
|
||||
// Failed to build BoxStore due to database file issue, store message;
|
||||
@@ -81,12 +92,12 @@ object ObjectBox {
|
||||
}
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
|
||||
Timber.tag(TAG)
|
||||
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
|
||||
Timber.tag(TAG)
|
||||
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
|
||||
// Enable ObjectBox Admin on debug builds.
|
||||
// https://docs.objectbox.io/data-browser
|
||||
Admin(boxStore).start(context.applicationContext)
|
||||
// Enable ObjectBox Admin on debug builds.
|
||||
// https://docs.objectbox.io/data-browser
|
||||
Admin(boxStore).start(context.applicationContext)
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -114,42 +125,166 @@ object ObjectBox {
|
||||
return true
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 安全获取 Food 实体 foodVector 字段的向量索引(推荐写法)
|
||||
// */
|
||||
// fun getFoodVectorIndex(box: Box<Food>): VectorIndex? {
|
||||
// return try {
|
||||
// // 核心修复:使用自动生成的 Food_.foodVector 而非字符串,避免拼写错误
|
||||
// val foodVectorProperty = Food_.foodVector
|
||||
// // 获取向量索引(适配 ObjectBox 3.5+ 所有版本)
|
||||
// foodVectorProperty.vectorIndex
|
||||
// } catch (e: Exception) {
|
||||
// // 容错处理:打印错误信息,避免崩溃
|
||||
// e.printStackTrace()
|
||||
// null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 刷新索引的调用示例(结合你之前的更新场景)
|
||||
// fun refreshFoodVectorIndex(box: Box<Food>) {
|
||||
// val vectorIndex = getFoodVectorIndex(box)
|
||||
// vectorIndex?.run {
|
||||
// // 同步刷新索引(解决查询异常问题)
|
||||
// refresh()
|
||||
// // 等待索引构建完成(超时 5 秒,避免无限等待)
|
||||
// waitUntilBuilt(5000)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 更新 isDel 字段后的完整调用流程
|
||||
// fun markFoodAsDeleted(box: Box<Food>, foodId: Long) {
|
||||
// val food = box[foodId]
|
||||
// if (food != null) {
|
||||
// food.isDel = true
|
||||
// box.put(food)
|
||||
// // 更新后刷新索引
|
||||
// refreshFoodVectorIndex(box)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 获取指定实体的Box,自动关联BoxStore
|
||||
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
|
||||
|
||||
// 协程安全执行数据库操作(推荐所有操作使用此方法)
|
||||
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
dbMutex.withLock { operation() }
|
||||
} catch (e: FileCorruptException) {
|
||||
// 操作中触发损坏,尝试重建数据库
|
||||
//val context = boxStore.context
|
||||
//deleteDbFiles(context)
|
||||
init(App.getContext()!!)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
|
||||
val query: Query<Food>? = getBox<Food>()?.query()
|
||||
?.equal(Food_.isDel, false)
|
||||
?.and()
|
||||
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
|
||||
?.build()
|
||||
try {
|
||||
query?.findWithScores()
|
||||
} finally {
|
||||
// 先关闭Query,释放Cursor
|
||||
query?.close()
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
suspend fun get(id: Long) = safeDbOp {
|
||||
getBox<Food>()?.get(id)
|
||||
}
|
||||
|
||||
suspend fun put(entity: Food) = safeDbOp {
|
||||
getBox<Food>()?.put(entity)
|
||||
}
|
||||
|
||||
suspend fun putAll(entities: List<Food>) = safeDbOp {
|
||||
getBox<Food>()?.put(entities)
|
||||
}
|
||||
|
||||
suspend fun getAll() = safeDbOp {
|
||||
getBox<Food>()?.all
|
||||
} ?: emptyList()
|
||||
|
||||
suspend fun filter(name: String?, isDistinct: Boolean = false) = safeDbOp {
|
||||
val list = if (isDistinct) {
|
||||
getBox<Food>()?.all?.distinctBy { it.foodName }
|
||||
} else {
|
||||
getBox<Food>()?.all
|
||||
}
|
||||
if (name.isNullOrBlank().not()) {
|
||||
list?.filter { it.foodName?.contains(name) == true }
|
||||
}
|
||||
list
|
||||
} ?: emptyList()
|
||||
|
||||
suspend fun removeAll() = safeDbOp {
|
||||
getBox<Food>()?.removeAll()
|
||||
}
|
||||
|
||||
suspend fun remove(name: String) = safeDbOp {
|
||||
getBox<Food>()?.run {
|
||||
val filterIdList = all.filter { it.foodName == name }.map { it.id }
|
||||
removeByIds(filterIdList)
|
||||
}
|
||||
}
|
||||
|
||||
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
|
||||
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
|
||||
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
|
||||
|
||||
// 检查存储是否可读写(操作数据库前调用)
|
||||
fun isStorageAvailable(context: Context): Boolean {
|
||||
return try {
|
||||
val state = Environment.getExternalStorageState()
|
||||
val innerDir = context.filesDir
|
||||
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 备份数据库到应用私有目录(无权限要求,推荐)
|
||||
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
|
||||
if (!isStorageAvailable(context)) return@withContext false
|
||||
val dbDir = File(context.filesDir, DB_DIR_NAME)
|
||||
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
|
||||
return@withContext copyDir(dbDir, backupDir)
|
||||
}
|
||||
|
||||
// 从备份恢复数据库(恢复后会重建BoxStore)
|
||||
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
|
||||
if (!isStorageAvailable(context)) return@withContext false
|
||||
val dbDir = File(context.filesDir, DB_DIR_NAME)
|
||||
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
|
||||
if (!backupDir.exists()) return@withContext false
|
||||
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
|
||||
boxStore?.close()
|
||||
deleteDbFiles(context)
|
||||
val isSuccess = copyDir(backupDir, dbDir)
|
||||
// 重新初始化
|
||||
init(context)
|
||||
return@withContext isSuccess
|
||||
}
|
||||
|
||||
// 递归删除数据库文件
|
||||
private fun deleteDbFiles(context: Context) {
|
||||
val dbDir = File(context.filesDir, DB_DIR_NAME)
|
||||
if (dbDir.exists()) deleteDirRecursively(dbDir)
|
||||
}
|
||||
|
||||
// 递归删除目录
|
||||
private fun deleteDirRecursively(file: File) {
|
||||
if (file.isDirectory) {
|
||||
file.listFiles()?.forEach { deleteDirRecursively(it) }
|
||||
}
|
||||
file.delete()
|
||||
}
|
||||
|
||||
// 递归复制目录(核心备份/恢复逻辑)
|
||||
private fun copyDir(srcDir: File, destDir: File): Boolean {
|
||||
return try {
|
||||
if (!srcDir.exists()) return false
|
||||
if (!destDir.exists()) destDir.mkdirs()
|
||||
srcDir.listFiles()?.forEach { srcFile ->
|
||||
val destFile = File(destDir, srcFile.name)
|
||||
if (srcFile.isDirectory) {
|
||||
copyDir(srcFile, destFile)
|
||||
} else {
|
||||
copyFile(srcFile, destFile)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制单个文件(使用NIO,高效稳定)
|
||||
private fun copyFile(srcFile: File, destFile: File) {
|
||||
FileInputStream(srcFile).channel.use { srcChannel ->
|
||||
FileOutputStream(destFile).channel.use { destChannel ->
|
||||
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭BoxStore(应用退出时调用,可选)
|
||||
fun close() {
|
||||
boxStore?.close()
|
||||
boxStore = null
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user