蓝牙、串口、USB、网络十余种通道,业务层真要逐个适配吗?
前言
在测绘、工业物联网这类垂直行业的 Android 应用里,同一个业务逻辑往往要同时适配蓝牙经典、BLE、串口、TCP、UDP、USB、NTRIP 等一大堆物理通道。
更麻烦的是,同一台设备还可能同时走两种通道——比如串口负责读定位数据、WiFi 负责写差分。要是每种通道都各写一套连接管理、数据收发、状态通知逻辑,代码量会急剧膨胀,维护成本直接失控。
我在自研的这套 GIS/地图引擎里,最终落地了一套多通道设备通信统一抽象架构,用三层抽象把 20+ 种设备收敛成一套接口,业务层对底层通道完全无感。这篇也是本系列的收尾篇,把通信层的工程化收尾讲清楚。
一、问题本质
在垂直行业(测绘、工业物联网、医疗设备等)的 Android 应用中,一个常见需求是:同一个业务逻辑,需要适配多种物理通信通道。
以测绘行业为例,一台 RTK 定位设备可以通过以下任意一种方式与 Android 手簿通信:
- 蓝牙经典(SPP)
- BLE
- USB 串口
- TCP Socket(WiFi/有线)
- UDP
- 内置串口(手簿内置接收机)
- NTRIP(网络差分)
而且,同一台设备可能同时使用两种通道——比如串口读数据、WiFi 写差分数据。如果每种通道都写一套独立的连接管理、数据收发、状态通知逻辑,代码会急剧膨胀,维护成本失控。
二、核心设计:三层抽象
2.1 架构总览
下面这张分层图是整个架构的骨架,从上到下依次是业务层、设备抽象层、IO 流抽象层和通道实现层。
┌──────────────────────────────────────────────────┐
│ 业务层 (API) │
│ 定位数据 · 设备状态 · 配置读写 · 差分数据发送 │
├──────────────────────────────────────────────────┤
│ 设备抽象层 (Device) │
│ 连接状态机 · 自动重连 · 事件分发 · 命令管理 │
├──────────────────────────────────────────────────┤
│ IO流抽象层 (IOStream) │
│ 异步读写线程 · 数据解析管道 · 帧过滤器 │
├──────────────────────────────────────────────────┤
│ 通道实现层 (Implementations) │
│ 蓝牙 · BLE · 串口 · Socket · UDP · USB · NTRIP │
└──────────────────────────────────────────────────┘
关键原则:每一层只依赖下一层的抽象接口,不依赖具体实现。
2.2 第一层:IO 流抽象
所有物理通道最终都归结为"字节流的读写"。因此,最底层抽象一个异步 IO 流:
下面这段先定义最底层的异步 IO 流抽象,只留 open/close/write 三个核心方法和一个读回调。
abstract class AsyncIOStream {
// 生命周期
abstract fun open()
abstract fun close()
// 写入
abstract fun write(data: ByteArray)
// 读取线程不断回调
protected var onReadCallback: ((ByteArray, Int) -> Unit)? = null
fun setOnReadCallback(callback: (ByteArray, Int) -> Unit) {
onReadCallback = callback
}
}
每种通道只需实现 open()、close()、write() 三个方法,并在内部启动读线程将数据通过回调抛出。
2.3 第二层:设备抽象
设备层是整个架构的核心。它管理连接状态机、自动重连、事件分发、数据解析器注册等横切关注点:
下面这段是设备抽象层的主体,用模板方法封装了连接/断开骨架,并内置了状态机和自动重连。
abstract class Device {
// ── 状态机 ──
enum class ConnectionState {
UNCONNECTED, // 未连接
CONNECTING, // 连接中
CONNECTED, // 已连接
DESTROYED // 已销毁
}
private var state = ConnectionState.UNCONNECTED
private val stateLock = Any()
// ── 自动重连 ──
private var autoReconnectEnabled = false
private var autoReconnectInterval = 3000L
private var reconnectJob: Job? = null
// ── 事件监听 ──
private val listeners = mutableListOf<DeviceEventListener>()
// ── 数据解析器链 ──
private val parsers = mutableListOf<Parser>()
// ── 公开方法 ──
fun connect() {
synchronized(stateLock) {
if (state != ConnectionState.UNCONNECTED) return
state = ConnectionState.CONNECTING
}
dispatchEvent(DeviceEvent.CONNECTING)
doConnect() // 模板方法,子类实现
}
fun disconnect() {
synchronized(stateLock) {
if (state == ConnectionState.DESTROYED) return
}
doDisconnect() // 模板方法,子类实现
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.DISCONNECTED)
cancelReconnect()
}
// ── 模板方法 ──
protected abstract fun doConnect()
protected abstract fun doDisconnect()
// ── 状态变更通知 ──
protected fun onConnected() {
synchronized(stateLock) {
state = ConnectionState.CONNECTED
}
dispatchEvent(DeviceEvent.CONNECTED)
}
protected fun onConnectFailed() {
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.CONNECT_FAILED)
tryAutoReconnect()
}
protected fun onDisconnected() {
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.DISCONNECTED)
tryAutoReconnect()
}
// ── 自动重连 ──
private fun tryAutoReconnect() {
if (!autoReconnectEnabled) return
cancelReconnect()
reconnectJob = CoroutineScope(Dispatchers.IO).launch {
delay(autoReconnectInterval)
connect()
}
}
fun setAutoReconnect(enabled: Boolean, intervalMs: Long = 3000L) {
autoReconnectEnabled = enabled
autoReconnectInterval = intervalMs
if (!enabled) cancelReconnect()
}
// ── 数据分发 ──
protected fun onDataReceived(data: ByteArray, length: Int) {
for (parser in parsers) {
try {
parser.parse(data, length)
} catch (e: Exception) {
// 解析异常不影响其他解析器
}
}
}
fun registerParser(parser: Parser) {
parsers.add(parser)
}
fun unRegisterParser(parser: Parser) {
parsers.remove(parser)
}
}
设计要点:
2.4 第三层:通道实现
以蓝牙设备为例,展示具体通道如何接入:
下面这段以蓝牙通道为例,只关心"怎么建连"和"怎么收发字节",业务逻辑一概不碰。
class BluetoothDevice(private val address: String) : Device() {
private var ioStream: BluetoothIOStream? = null
override fun doConnect() {
CoroutineScope(Dispatchers.IO).launch {
try {
val socket = BluetoothAdapter.getDefaultAdapter()
.getRemoteDevice(address)
.createRfcommSocketToServiceRecord(SPP_UUID)
socket.connect()
ioStream = BluetoothIOStream(socket)
ioStream?.setOnReadCallback { data, len ->
onDataReceived(data, len)
}
ioStream?.open()
onConnected()
} catch (e: Exception) {
onConnectFailed()
}
}
}
override fun doDisconnect() {
ioStream?.close()
ioStream = null
}
fun sendData(data: ByteArray): Boolean {
ioStream?.write(data) ?: return false
return true
}
}
关键:通道实现只关心"如何建立连接"和"如何收发字节",不关心业务逻辑。
三、IO 适配器:读写分离
在实际项目中,有一种特殊需求:读取和写入走不同的通道。
例如,某款手簿的内置接收机通过串口输出定位数据,但写入差分数据需要通过 UDP 发送到另一个模块。此时,一个设备需要同时管理两个通道——一个负责读、一个负责写。
3.1 适配器模式
下面这段用适配器把"读通道、写通道、UDP 写通道"组合进同一个设备对象,对外还是一套接口。
class IOAdapterDevice(
private val readDevice: Device?, // 读通道
private val writeDevice: Device?, // 写通道
private val udpDevice: Device? // UDP 写通道(可选)
) : Device() {
// 连接:依次连接所有通道
override fun doConnect() {
powerOn()
readDevice?.connect()
writeDevice?.connect()
udpDevice?.connect()
}
// 断开:依次断开所有通道
override fun doDisconnect() {
readDevice?.disconnect()
writeDevice?.disconnect()
udpDevice?.disconnect()
powerOff()
}
// 发送命令:走写通道
fun sendCommand(cmd: Command) {
writeDevice?.sendCommand(cmd)
}
// 发送差分数据:走写通道或 UDP
fun sendDifferentialData(data: ByteArray) {
writeDevice?.sendDifferentialData(data)
udpDevice?.sendData(data)
}
// 读取数据:走读通道
fun getReceivedData(): Data? {
return readDevice?.getData()
}
}
3.2 初始化命令队列
某些设备在连接后需要发送一系列初始化命令,且命令之间有严格的时序要求(必须等上一条命令返回 OK 后才能发送下一条)。这通过"命令队列 + 响应驱动"实现:
下面这段用"命令队列 + 响应驱动"保证初始化命令严格按序发送,队列跑完自动注销监听器。
class CommandQueueManager(private val device: Device) {
private val commandQueue = ArrayDeque<Command>()
private var firstCommandSent = false
fun enqueue(commands: List<Command>) {
commandQueue.addAll(commands)
// 注册一个临时解析器,监听设备响应
device.registerParser(object : Parser {
override fun parse(data: ByteArray, length: Int) {
val response = String(data, 0, length)
if (response.isEmpty()) return
if (!firstCommandSent) {
sendNext()
firstCommandSent = true
} else if (response.contains("OK") || response.contains("ERROR")) {
sendNext()
}
if (commandQueue.isEmpty()) {
device.unRegisterParser(this) // 队列清空,注销解析器
}
}
override fun getData(): Data? = null
})
}
private fun sendNext() {
commandQueue.poll()?.let { cmd ->
device.sendCommand(cmd)
}
}
}
设计亮点:解析器本身就是"观察者",当命令队列清空后自动注销,不污染正常的数据解析流程。
四、构建器模式:统一创建入口
20+ 种设备类型,如果每种都直接 new,创建逻辑会散落在业务代码各处。用构建器模式统一创建入口:
下面这段用构建器把各类设备的创建收拢到统一入口,业务层不再直接 new 具体类型。
abstract class DeviceBuilder {
var content: DeviceContent? = null
fun content(content: DeviceContent): DeviceBuilder {
this.content = content
return this
}
abstract fun build(): Device
}
// 蓝牙设备构建器
class BluetoothDeviceBuilder : DeviceBuilder() {
override fun build(): Device {
return BluetoothDevice(content)
}
}
// Socket 设备构建器
class SocketDeviceBuilder : DeviceBuilder() {
override fun build(): Device {
return SocketDevice(content)
}
}
// 使用
val device = BluetoothDeviceBuilder()
.content(deviceContent)
.build()
device.connect()
更进一步,可以根据设备类型枚举,用工厂方法自动选择构建器:
下面这段用工厂方法按设备类型枚举自动挑构建器,新增类型只需补一个分支。
fun createBuilder(type: DeviceType): DeviceBuilder {
return when (type) {
DeviceType.BLUETOOTH -> BluetoothDeviceBuilder()
DeviceType.BLE -> BleDeviceBuilder()
DeviceType.SOCKET -> SocketDeviceBuilder()
DeviceType.SERIAL_PORT -> SerialPortDeviceBuilder()
DeviceType.UDP -> UdpDeviceBuilder()
// … 其他类型
else -> throw IllegalArgumentException("Unknown device type: $type")
}
}
五、状态持久化:Content 机制
设备的状态(配置参数、工作模式、连接信息等)需要持久化存储,并在进程间共享。这通过一个类似 SharedPreferences 的 Content 机制实现:
下面这段定义了一套类似 SharedPreferences 的 Content 接口,并在基类里用 getSharedPreferences 落地。
interface DeviceContent {
fun <T> get(key: String, defaultValue: T): T
fun <T> set(key: String, value: T)
fun contains(key: String): Boolean
fun remove(key: String)
}
class BaseDeviceContent(context: Context) : DeviceContent {
private val prefs = context.getSharedPreferences("device_content", Context.MODE_PRIVATE)
override fun <T> get(key: String, defaultValue: T): T {
@Suppress("UNCHECKED_CAST")
return when (defaultValue) {
is String -> prefs.getString(key, defaultValue) as T
is Int -> prefs.getInt(key, defaultValue) as T
is Long -> prefs.getLong(key, defaultValue) as T
is Float -> prefs.getFloat(key, defaultValue) as T
is Boolean -> prefs.getBoolean(key, defaultValue) as T
else -> throw IllegalArgumentException("Unsupported type")
}
}
override fun <T> set(key: String, value: T) {
prefs.edit().apply {
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is Boolean -> putBoolean(key, value)
else -> throw IllegalArgumentException("Unsupported type")
}
}.apply()
}
// … 其他方法
}
业务层通过 Content 接口读写设备配置,完全不需要知道底层是 SharedPreferences 还是其他存储方式。
六、架构收益
下面是这套架构在几个关键维度上的收益速查表:
| 扩展性 | 新增通道类型只需实现 IOStream + 继承 Device,零改动上层代码 |
| 可维护性 | 每种通道的连接逻辑独立,互不干扰 |
| 可测试性 | 可以 Mock Device 进行业务层测试,无需真实硬件 |
| 复用性 | IO 适配器模式让同一设备灵活组合不同通道 |
| 稳定性 | 状态机 + 自动重连保证连接可靠性 |
七、结论
- 彻底解耦:把"通信通道"和"业务逻辑"拆开,业务层不感知底层走的是蓝牙还是串口
- 三层职责:IO 流层只管收发字节,设备层只管连接状态,适配器层负责组合通道
- 零改动扩展:新增通道类型只需实现 IOStream 并继承 Device,上层代码一行都不用改
- 统一入口:构建器 + 工厂方法把 20+ 种设备的创建收拢到一处,避免散落
- 可测可维护:Mock Device 即可做业务层单元测试,每种通道的连接逻辑互不影响
以上就是这套多通道统一抽象架构的全部内容。如果你在项目中也遇到过"一种设备 N 种通道"的维护噩梦,欢迎在评论区聊聊你最后是怎么收场的。
关键词标签:#Android #多通道通信 #蓝牙 #串口 #AIDL #架构设计 #Kotlin #设备抽象
网硕互联帮助中心




评论前必须登录!
注册