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