改造objectbox
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user