refactor(scale): 修复线程安全、生命周期与连接管理问题
- ScaleWebSocketServer:三个状态 Map 改为 ConcurrentHashMap,connectionCount 改为 AtomicInteger - ScaleServiceManager:新增 serviceScope 统一管理协程生命周期,新增 @Volatile isStarted 防重复启动 - ScaleWebSocketClient:doConnect 写入新连接前先 close() 旧连接,防止 TCP 资源泄漏 - MdnsDiscoveryManager:移除 isResolving 上多余的 @Volatile 注解 - MasterScaleActivity:全限定类名改为 import + 短类名 - WeightUtil:删除 WeightListenerImpl 中注释掉的调试代码 - scale包架构分析.md:同步更新架构文档,已修复问题压缩为汇总表 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,6 @@ class MdnsDiscoveryManager(private val context: Context) {
|
||||
|
||||
/** 待解析队列,串行处理避免并发解析失败 */
|
||||
private val resolveQueue = LinkedList<NsdServiceInfo>()
|
||||
@Volatile
|
||||
private var isResolving = false
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.shuwei.dish.match.utils.NetworkUtil
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -34,6 +36,9 @@ object ScaleServiceManager {
|
||||
|
||||
private const val TAG = "ScaleServiceManager"
|
||||
|
||||
@Volatile private var isStarted = false
|
||||
private var serviceScope: CoroutineScope? = null
|
||||
|
||||
private var mdnsRegister: MdnsRegisterManager? = null
|
||||
private var wsServer: ScaleWebSocketServer? = null
|
||||
private var udpSender: UdpBroadcastSender? = null
|
||||
@@ -135,6 +140,9 @@ object ScaleServiceManager {
|
||||
* 启动所有秤服务,在 Application.onCreate() 中调用
|
||||
*/
|
||||
fun start(context: Context) {
|
||||
if (isStarted) return
|
||||
isStarted = true
|
||||
serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
val deviceId = GlobalData.deviceId
|
||||
val role = GlobalData.deviceRole
|
||||
Log.d(TAG, "启动秤服务, deviceId=$deviceId, role=$role")
|
||||
@@ -180,14 +188,14 @@ object ScaleServiceManager {
|
||||
}
|
||||
it.onDeviceConnected = { remoteId ->
|
||||
// 子设备连接成功时,从 Room 读取全量配置并单独推送给该设备
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val slots = BaseApp.instance!!.database.seasoningSlotDao()
|
||||
.queryAll()
|
||||
.map { slot ->
|
||||
serviceScope?.launch {
|
||||
val slots = BaseApp.instance?.database?.seasoningSlotDao()
|
||||
?.queryAll()
|
||||
?.map { slot ->
|
||||
ScaleEvent.SlotConfig(
|
||||
slot.deviceId, slot.address, slot.goodsId, slot.goodsName
|
||||
)
|
||||
}
|
||||
} ?: return@launch
|
||||
if (slots.isNotEmpty()) sendSeasoningConfigTo(remoteId, slots)
|
||||
}
|
||||
}
|
||||
@@ -246,6 +254,9 @@ object ScaleServiceManager {
|
||||
* 停止所有秤服务,在 Application.onTerminate() 或退出时调用
|
||||
*/
|
||||
fun stop() {
|
||||
serviceScope?.cancel()
|
||||
serviceScope = null
|
||||
isStarted = false
|
||||
mdnsDiscovery?.stopDiscovery()
|
||||
udpReceiver?.stop()
|
||||
udpSender?.stop()
|
||||
|
||||
@@ -138,7 +138,8 @@ class ScaleWebSocketClient {
|
||||
}
|
||||
})
|
||||
|
||||
connections[deviceId] = ws
|
||||
// 写入新连接前先关闭旧连接,避免 IP 变化时旧 ws 对象悬空造成 TCP 资源泄漏
|
||||
connections.put(deviceId, ws)?.close(1000, "新连接替换")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,11 @@ import org.java_websocket.WebSocket
|
||||
import org.java_websocket.handshake.ClientHandshake
|
||||
import org.java_websocket.server.WebSocketServer
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* 秤数据 WebSocket 服务端(主设备和子设备均运行)
|
||||
@@ -46,13 +48,13 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
get() = server?.connections?.isNotEmpty() == true
|
||||
|
||||
/** 每个秤地址的最近一次推送时间,用于节流 */
|
||||
private val lastPushTime = mutableMapOf<Int, Long>()
|
||||
private val lastPushTime = ConcurrentHashMap<Int, Long>()
|
||||
|
||||
/** 缓存各秤最新数据,新客户端连接时立即同步当前状态 */
|
||||
private val latestData = mutableMapOf<Int, ScaleData>()
|
||||
private val latestData = ConcurrentHashMap<Int, ScaleData>()
|
||||
|
||||
/** 节流窗口内的延迟推送任务,key=address;保证窗口结束时最新数据被推送 */
|
||||
private val pendingPushTasks = mutableMapOf<Int, ScheduledFuture<*>>()
|
||||
private val pendingPushTasks = ConcurrentHashMap<Int, ScheduledFuture<*>>()
|
||||
|
||||
/**
|
||||
* 启动 WebSocket 服务器,并注册 WeightUtil 回调
|
||||
@@ -67,7 +69,7 @@ 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()
|
||||
// 方案三:数据未变化时直接跳过,避免无效推送
|
||||
// 数据未变化时直接跳过,避免无效推送
|
||||
val last = latestData[address]
|
||||
if (last?.weight == weight && last.state == state) return@addWeightListener
|
||||
|
||||
@@ -89,7 +91,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
pendingPushTasks.remove(address)?.cancel(false)
|
||||
broadcast(gson.toJson(data))
|
||||
} else if (pendingPushTasks[address] == null) {
|
||||
// 方案一:窗口内首次触发时安排一个 trailing 推送,保证窗口末尾推最新值
|
||||
// 窗口内首次触发:安排 trailing 推送,保证窗口末尾推最新值
|
||||
val delay = THROTTLE_MS - (now - lastPush)
|
||||
if (!scheduler.isShutdown) {
|
||||
pendingPushTasks[address] = scheduler.schedule({
|
||||
@@ -156,10 +158,10 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
WebSocketServer(InetSocketAddress(port)) {
|
||||
|
||||
/** 当前活跃连接计数,用于替代 connections.isNullOrEmpty() 避免竞态 */
|
||||
private var connectionCount = 0
|
||||
private val connectionCount = AtomicInteger(0)
|
||||
|
||||
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
||||
connectionCount++
|
||||
connectionCount.incrementAndGet()
|
||||
Log.d(TAG, "主设备已连接: ${conn.remoteSocketAddress}")
|
||||
onConnectionChanged?.invoke(true)
|
||||
|
||||
@@ -194,8 +196,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
|
||||
override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
|
||||
Log.d(TAG, "主设备已断开: ${conn.remoteSocketAddress}, reason=$reason")
|
||||
// 用计数器判断是否还有活跃连接,避免依赖 connections 集合的移除时机导致竞态
|
||||
if (--connectionCount <= 0) {
|
||||
connectionCount = 0
|
||||
if (connectionCount.decrementAndGet() <= 0) {
|
||||
connectionCount.set(0)
|
||||
onConnectionChanged?.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
|
||||
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
|
||||
import com.shuwei.dish.match.databinding.ListTypeScale1Binding
|
||||
@@ -99,7 +100,7 @@ class MasterScaleActivity : BaseActivity() {
|
||||
val flow = ScaleServiceManager.allScales ?: return
|
||||
lifecycleScope.launch {
|
||||
flow.collectLatest { scaleMap ->
|
||||
val localId = com.shuwei.dish.match.base.GlobalData.deviceId
|
||||
val localId = GlobalData.deviceId
|
||||
val localIp = NetworkUtil.getLocalIpAddress(this@MasterScaleActivity)
|
||||
val groups = scaleMap.values
|
||||
.groupBy { it.deviceId }
|
||||
|
||||
@@ -140,14 +140,6 @@ open class WeightListenerImpl : Weigher2.Listener {
|
||||
}
|
||||
|
||||
override fun onGetWeight(address: Int, state: Int, weight: Double) {
|
||||
// val stateStr = when (state) {
|
||||
// SensorScale.STATE_STABLE -> "稳定"
|
||||
// SensorScale.STATE_UNSTABLE -> "不稳定"
|
||||
// SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
|
||||
// else -> ""
|
||||
// }
|
||||
// val result1 = String.format("$stateStr 重量:%s kg", weight / 1000f)
|
||||
// Log.d("WeightUtil", "ttlReturn, address:$address,$result1")
|
||||
}
|
||||
|
||||
override fun onReadIdentify(rate: Int) {
|
||||
|
||||
Reference in New Issue
Block a user