diff --git a/app/src/main/java/com/sw/inbound/objbox/FoodModule.kt b/app/src/main/java/com/sw/inbound/objbox/FoodModule.kt index 45022bd..df93c55 100644 --- a/app/src/main/java/com/sw/inbound/objbox/FoodModule.kt +++ b/app/src/main/java/com/sw/inbound/objbox/FoodModule.kt @@ -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 +// private lateinit var box: Box private lateinit var embeddingsList: List> 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() + 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 { + suspend fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List { if (floatArray == null) return emptyList() - val query: Query = - 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 - try { - idScoreList = query.findIdsWithScores(); - } finally { - // 先关闭Query,释放Cursor - query.close() - } +// val query: Query = 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 +// try { +// idScoreList = query.findIdsWithScores(); +// } finally { +// // 先关闭Query,释放Cursor +// query.close() +// } + val idScoreList = ObjectBox.query(floatArray, queryCount) val nameScoreList = mutableListOf() 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 { - val floatArray = bitmap2FloatArray(bitmap, false) - if (floatArray == null) return emptyList() + suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List { + 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 { + suspend fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List { val map = mutableMapOf() 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()?.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() 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() } diff --git a/app/src/main/java/com/sw/inbound/objbox/ObjectBox.kt b/app/src/main/java/com/sw/inbound/objbox/ObjectBox.kt index 6a6f38c..d034dbe 100644 --- a/app/src/main/java/com/sw/inbound/objbox/ObjectBox.kt +++ b/app/src/main/java/com/sw/inbound/objbox/ObjectBox.kt @@ -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 getBox(): Box? = boxStore?.boxFor() + + // 协程安全执行数据库操作(推荐所有操作使用此方法) + suspend fun 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? = getBox()?.query( + Food_.foodVector + .nearestNeighbors(floatArray, queryCount) + )?.build() + try { + query?.findIdsWithScores() + } finally { + // 先关闭Query,释放Cursor + query?.close() + } + } ?: emptyList() + + suspend fun get(id: Long) = safeDbOp { + getBox()?.get(id) + } + + suspend fun put(entity: Food) = safeDbOp { + getBox()?.put(entity) + } + + suspend fun putAll(entities: List) = safeDbOp { + getBox()?.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 + } + + } \ No newline at end of file