From d412e03c6ef5779f733ed047f5fffbfc2b520fce Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Tue, 12 May 2026 14:07:22 +0800 Subject: [PATCH] =?UTF-8?q?perf(activity):=20=E4=BC=98=E5=8C=96=E7=A7=A4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=20StateFlow=20=E9=87=8D=E5=8F=91=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E6=97=A0=E6=95=88=E9=81=8D=E5=8E=86=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20ts=20=E5=8E=BB=E9=87=8D=E8=B7=B3=E8=BF=87=E6=9C=AA?= =?UTF-8?q?=E5=8F=98=E5=8C=96=E5=B8=A7=EF=BC=9BScaleWebSocketServer=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20trailing=20=E8=8A=82=E6=B5=81=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=8F=98=E5=8C=96=E5=8E=BB?= =?UTF-8?q?=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dish/match/scale/ScaleWebSocketServer.kt | 35 +++++++++-- .../dish/match/ui/SubmitFoodActivity.kt | 62 ++++++++++++++----- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt index 674eed2..eabb9a3 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleWebSocketServer.kt @@ -10,6 +10,7 @@ import org.java_websocket.handshake.ClientHandshake import org.java_websocket.server.WebSocketServer import java.net.InetSocketAddress import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit /** @@ -50,6 +51,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co /** 缓存各秤最新数据,新客户端连接时立即同步当前状态 */ private val latestData = mutableMapOf() + /** 节流窗口内的延迟推送任务,key=address;保证窗口结束时最新数据被推送 */ + private val pendingPushTasks = mutableMapOf>() + /** * 启动 WebSocket 服务器,并注册 WeightUtil 回调 */ @@ -61,11 +65,12 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co // 注册本机秤数据监听 WeightUtil.addWeightListener(TAG) { address, state, weight -> + Log.d(TAG, "WebSocket 设备${deviceId}推送数据, address=$address, state=$state, weight=$weight") val now = System.currentTimeMillis() - // 节流:同一地址 100ms 内只推送一次 - if (now - (lastPushTime[address] ?: 0L) < THROTTLE_MS) return@addWeightListener + // 方案三:数据未变化时直接跳过,避免无效推送 + val last = latestData[address] + if (last?.weight == weight && last.state == state) return@addWeightListener - lastPushTime[address] = now val data = ScaleData( deviceId = deviceId, address = address, @@ -74,8 +79,28 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co ts = now, ip = NetworkUtil.getLocalIpAddress(context) ) + // 始终更新 latestData,保证最新一帧不丢失 latestData[address] = data - broadcast(gson.toJson(data)) + + val lastPush = lastPushTime[address] ?: 0L + if (now - lastPush >= THROTTLE_MS) { + // 窗口已过:立即推送,并取消可能存在的延迟任务 + lastPushTime[address] = now + pendingPushTasks.remove(address)?.cancel(false) + broadcast(gson.toJson(data)) + } else if (pendingPushTasks[address] == null) { + // 方案一:窗口内首次触发时安排一个 trailing 推送,保证窗口末尾推最新值 + val delay = THROTTLE_MS - (now - lastPush) + if (!scheduler.isShutdown) { + pendingPushTasks[address] = scheduler.schedule({ + val latest = latestData[address] ?: return@schedule + lastPushTime[address] = System.currentTimeMillis() + pendingPushTasks.remove(address) + broadcast(gson.toJson(latest)) + }, delay, TimeUnit.MILLISECONDS) + } + } + // 窗口内且已安排过 trailing 任务:仅更新 latestData,等 trailing 触发时推送 } } @@ -96,6 +121,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co server = null latestData.clear() lastPushTime.clear() + pendingPushTasks.values.forEach { it.cancel(false) } + pendingPushTasks.clear() Log.d(TAG, "WebSocket 服务端已停止") } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt index 06f4d91..e680af6 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt @@ -95,6 +95,11 @@ class SubmitFoodActivity : BaseActivity() { */ private val baseSeasoningMap = mutableMapOf>() + /** + * 各秤上一次处理过的 ts,用于跳过 StateFlow 因其他秤变化而重发的相同帧 + */ + private val lastTsMap = mutableMapOf() + @Suppress("unchecked_cast", "DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -175,7 +180,8 @@ class SubmitFoodActivity : BaseActivity() { // 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter val foodId = food?.foodId ?: return@launch val cookMode = food?.cookMode ?: return@launch - val list = appViewModel.getCookFoodGoodsList(foodId, cookMode, food?.dinnerType ?: "0") + val list = + appViewModel.getCookFoodGoodsList(foodId, cookMode, food?.dinnerType ?: "0") goodsList = list prefillSeasoningFromGoodsList(list) } else { @@ -213,13 +219,20 @@ class SubmitFoodActivity : BaseActivity() { val flow = ScaleServiceManager.allScales ?: return lifecycleScope.launch { flow.collectLatest { scaleMap -> + //Log.d(TAG, "observeScaleData,scaleMap=${scaleMap.toJsonString()}") scaleMap.forEach { (key, data) -> + // ts 未变化说明是其他秤更新触发的 StateFlow 重发,跳过 + if (lastTsMap[key] == data.ts) return@forEach + lastTsMap[key] = data.ts // 排除主设备 2格秤 if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach // 从槽位配置读取调料名,未配置则跳过 val (goodsId, goodsName) = slotMap[key] ?: return@forEach - + Log.d( + TAG, + "observeScaleData,1key=$key, goodsId=$goodsId, goodsName=$goodsName, data=$data" + ) // 首次收到稳定数据时记录基准重量 val baseline = baselineMap[key] if (baseline == null) { @@ -228,19 +241,28 @@ class SubmitFoodActivity : BaseActivity() { } return@forEach } - + Log.d(TAG, "observeScaleData,2key=$key, baseline=$baseline, data.weight=${data.weight}") // 用量 = 初始重量 - 当前重量,负值归零 val useWeight = (baseline - data.weight) .coerceAtLeast(0.0) .roundedOneDecimalPlace() - // 更新原始用量 map,再按 goodsName 聚合刷新 adapter if (useWeight < 0.5) { - rawWeightMap.remove(key) + // 放回场景:必须稳定后才移除,避免拿取过程中误删 + if (data.state != WeightUtil.STATE_STABLE) return@forEach + if (rawWeightMap.remove(key) != null) { + Log.d(TAG, "observeScaleData,移除key=${key}, goodsName=$goodsName") + refreshAdapterByName(goodsName) + } } else { - rawWeightMap[key] = Triple(goodsId, goodsName, useWeight) + // 拿取场景:实时响应,无需等待稳定 + val newTriple = Triple(goodsId, goodsName, useWeight) + if (rawWeightMap[key] != newTriple) { + Log.d(TAG, "observeScaleData,新增:$newTriple") + rawWeightMap[key] = newTriple + refreshAdapterByName(goodsName) + } } - refreshAdapterByName(goodsName) } } } @@ -249,27 +271,35 @@ class SubmitFoodActivity : BaseActivity() { /** * 按 goodsName 聚合 rawWeightMap,叠加历史用量后推给 adapter * isCooking=true 时只新增,不移除(历史调料数据不可减少) + * 移除前检查 item 是否存在,更新前比较数据是否变化,避免无效操作 */ private fun refreshAdapterByName(goodsName: String) { val grouped = rawWeightMap.values.filter { it.second == goodsName } val newWeight = grouped.sumOf { it.third }.roundedOneDecimalPlace() val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0) val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace() + Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}") if (totalWeight < 0.5) { - // 制作中模式:已有历史数据,不允许移除 - if (food?.isCooking != true) { - seasoningAdapter.removeItem(goodsName) + Log.d(TAG, "refreshAdapterByName,重量小于0.5g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight") + // 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除 + if (!baseSeasoningMap.containsKey(goodsName)) { + // 只有 item 确实存在时才调用 remove,避免无效遍历 + if (seasoningAdapter.items.any { it.goodsName == goodsName }) { + seasoningAdapter.removeItem(goodsName) + } } return } val goodsId = grouped.firstOrNull()?.first?.takeIf { it.isNotEmpty() } ?: baseGoodsId - seasoningAdapter.updateItem( - SeasoningWeightAdapter.Item( - goodsId = goodsId, - goodsName = goodsName, - useWeight = totalWeight - ) + // 数据未变化时跳过,避免触发不必要的 RecyclerView 刷新 + if (seasoningAdapter.items.any { it.goodsName == goodsName && it.useWeight == totalWeight && it.goodsId == goodsId }) return + val item = SeasoningWeightAdapter.Item( + goodsId = goodsId, + goodsName = goodsName, + useWeight = totalWeight ) + Log.d(TAG, "refreshAdapterByName,更新数据item=${item.toJsonString()}") + seasoningAdapter.updateItem(item) } private fun addViewListener() {