改造objectbox

This commit is contained in:
2026-01-31 13:17:13 +08:00
parent 6439720182
commit 4322c9495b
2 changed files with 193 additions and 36 deletions
@@ -15,6 +15,8 @@ import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.IdWithScore
import io.objectbox.query.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly
import org.pytorch.IValue
import org.pytorch.Module
@@ -29,7 +31,7 @@ import java.io.InputStream
object FoodModule {
private lateinit var module_mobile: Module
private lateinit var box: Box<Food>
// private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
private lateinit var classInfo: FoodClassInfo
@@ -42,17 +44,14 @@ object FoodModule {
const val BAG_RATE = 0.05
fun init(context: Context) {
Thread {
suspend fun init(context: Context) {
withContext(Dispatchers.IO) {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
if (box.all.isEmpty()) {
val box = ObjectBox.getBox<Food>()
if (box?.all?.isEmpty() == true) {
initDefFoodData(context)
}
}.start()
}
}
// fun uri2FloatArray(uri: Uri): FloatArray? {
@@ -113,29 +112,29 @@ object FoodModule {
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List<IdNameScore> {
suspend fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List<IdNameScore> {
if (floatArray == null) return emptyList()
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
var idScoreList: List<IdWithScore>
try {
idScoreList = query.findIdsWithScores();
} finally {
// 先关闭Query,释放Cursor
query.close()
}
// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
// //查询比较分数
//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// var idScoreList: List<IdWithScore>
// try {
// idScoreList = query.findIdsWithScores();
// } finally {
// // 先关闭Query,释放Cursor
// query.close()
// }
val idScoreList = ObjectBox.query(floatArray, queryCount)
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
val name = ObjectBox.get(it.id)?.name?:""
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
}
Timber.tag("FoodModule").d("queryFood数据:${nameScoreList.toJsonString()}")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false)
if (floatArray == null) return emptyList()
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
@@ -162,7 +161,7 @@ object FoodModule {
Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
suspend fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
val map = mutableMapOf<String, Int>()
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
nameScoreList.filter { it.score < 0.05 }.forEach {
@@ -179,8 +178,8 @@ object FoodModule {
val score: Double
)
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
suspend fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = ObjectBox.getBox<Food>()?.all?.count { it.foodIdx == DEFAULT_FOOD_INDEX }?:0
if (count > 0) {
return
}
@@ -195,14 +194,17 @@ object FoodModule {
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
val list = mutableListOf<Food>()
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
ObjectBox.boxStore.runInTx {
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
// ObjectBox.boxStore.runInTx {
// box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// }
list.add(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
ObjectBox.putAll(list)
action()
}
@@ -17,14 +17,29 @@
package com.sw.inbound.objbox
import android.content.Context
import android.os.Environment
import android.util.Log
import com.sw.inbound.MyApp
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.IdWithScore
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.GZIPOutputStream
import kotlin.collections.emptyList
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
@@ -45,8 +60,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.
@@ -55,13 +69,16 @@ object ObjectBox {
private set
fun init(context: Context) {
if (boxStore != null && boxStore!!.isClosed.not()) return
// 优化:调大事务回收超时时间
System.setProperty("objectbox.finalizerTimeout", "30000");
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
.androidContext(context.applicationContext)
.disableMultiProcess() // 开启多进程支持(按需关闭,单进程可删)
.disableFileCompression() // 禁用文件压缩,避免存储异常
.build()
} catch (e: DbException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
@@ -76,7 +93,10 @@ object ObjectBox {
if (com.sw.inbound.BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
Log.d(
TAG,
"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)"
)
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
io.objectbox.android.Admin(boxStore).start(context.applicationContext)
@@ -107,4 +127,139 @@ object ObjectBox {
return true
}
// 获取指定实体的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(MyApp.instance!!)
null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
suspend fun query(floatArray: FloatArray?, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query(
Food_.foodVector
.nearestNeighbors(floatArray, queryCount)
)?.build()
try {
query?.findIdsWithScores()
} 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)
}
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
}
}