设备的独占性消除不了,进程隔离只是把「竞争」变成「排队」。
你手上有一台外接的定位设备,只有一个物理串口或蓝牙连接。UI 主进程要连它收定位,后台同步进程也要读它的实时状态,同厂的另一个 App 还想拿同一份数据。每个进程都说「这是我的」——可硬件只认一个主人。
最常见的直觉反应是各连各的。结果呢?蓝牙是一对一的,第二个进程根本连不上;串口是互斥的,谁先打开后面就打不开。你以为抢到了资源,设备直接罢工。文件共享轮询?实时性垮了,流数据和事件根本传不过去。
这篇文章要解决一个很具体的麻烦:一个物理上只能被一个人持有的设备,怎么让 N 个进程像「共用」它一样——实时拿数据、能收事件、还能双向发指令,而且不重复连硬件。先说结论,设备的独占性是物理事实,进程隔离只是把它从「竞争」变成「排队」,所以共享服务真正要解决的不是通信,而是所有权与生命周期。
一、为什么一个设备会被多个进程抢
在工业级 Android 应用里,多进程架构太常见了。典型角色分三类:
- 主进程:负责 UI、设备连接、数据采集,它是唯一真正握着设备连接的进程。
- 辅助进程:负责后台服务、数据同步、消息推送,逻辑上跟主进程分离,但它也要读设备状态和定位数据。
- 其他应用:同一厂商的其他 App 也需要访问同一台设备,跨 App 还要再共享一次。
核心矛盾就一句话:硬件设备只有一个连接,但多个进程都要访问。这不是你代码写得好不好就能绕开的,它是物理层的限制。
为什么工业 App 天然就是多进程?不是为了炫技,是被迫的。UI 进程要是崩了,后台同步不能跟着死,所以拆出去;消息推送要是被系统杀内存,不能把采集链路一起带走,再拆一层;同厂另一个 App 是独立 APK,根本不在你进程里。进程拆开带来崩溃隔离和保活好处,但代价就是「同一个设备,谁都摸不到对方的地址空间」——内存里的设备对象在 B 进程里根本不存在,你只能隔着 Binder 去借。
有人会想,那每个进程独立连一次不就行了?下表把四种直觉方案摆出来对比一下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 每个进程独立连接 | 简单 | 蓝牙只能一对一,串口互斥,无法实现 |
| 文件/SP 共享 | 简单 | 实时性差,无法传递事件和流数据 |
| Broadcast | Android 原生 | 延迟高,数据量受限,无法双向调用 |
| AIDL 服务 | 实时、双向、可传递流数据 | 实现复杂,需要处理进程间通信 |
再看一张表,把「进程内独占」和「跨进程共享服务」放到同一个维度上掰开说,你会更清楚为什么后者是唯一能落地的路:
| 维度 | 进程内独占 | 跨进程共享服务 |
|---|---|---|
| 硬件连接数 | 每进程各连一次,物理上不可行 | 服务端唯一连接,客户端共用 |
| 数据共享方式 | 同进程内存直接访问 | AIDL 跨进程传递 |
| 事件实时性 | 最高 | 毫秒级,接近同进程 |
| 双向调用 | 直接方法调用 | AIDL 接口双向 |
| 崩溃隔离 | 一个崩全崩 | 客户端崩不影响服务端设备 |
| 接入成本 | 低 | 中,需定义 AIDL 接口 |
结论很明确:AIDL 是最适合硬件设备共享场景的方案。它实时、双向、能传流数据,代价只是你要多花点心思处理进程间通信。后面整篇都是在讲这套通信怎么搭才稳。
二、整体架构:服务端持真设备,AIDL 做远程代理
先讲清楚一个核心思路,也是整套架构的命根子:服务端持有真实设备对象,AIDL 接口是设备对象的远程代理。客户端不碰硬件,它只持有一个「长得和真设备一样」的本地对象,所有方法调用通过 AIDL 转发到服务端,由服务端替它操作真实设备。
整体结构长这样:
flowchart TD
CB["客户端进程 B 业务代码"] --> CP["代理设备对象 ProxyDevice"]
CP -->|"AIDL 绑定与调用"| SS["设备共享服务 DeviceServerService"]
SB["服务端进程 A 业务代码"] --> SS
SS --> RD["真实设备对象 Device"]
RD --> HW["物理设备 唯一连接"]
SS -->|"事件回调 onEvent"| CP
三个角色必须分清楚,后面所有代码都围绕它们转:
- 服务端:持有真实设备连接的进程,跑一个
DeviceServerService。 - 客户端:通过 AIDL 绑定服务端,拿到设备代理对象。
- 设备代理:客户端本地对象,它的方法调用通过 AIDL 转发到服务端。
这里有个容易忽略的点:设备到底「归谁占着」。物理连接只能有一个主人(服务端),客户端只是排队借用同一个代理。把占用和释放画成状态机,生命周期就清楚了:
stateDiagram-v2
[*] --> 空闲
空闲 --> 已占用 : connect 调用
已占用 --> 数据流动 : 连接成功
数据流动 --> 已占用 : 断开或异常
已占用 --> 空闲 : disconnect
空闲 --> [*]
状态机的含义很简单:只有服务端能执行 connect 真正占用设备;连接成功后进入数据流动态;一旦断开或异常,回到已占用或空闲。客户端无论有多少个,看到的都是这同一个状态,因为它们共享的是同一个服务端。
顺带说一个 Binder 层面的细节,免得你后面踩坑:AIDL 调用默认是同步阻塞的,调用方线程会一直挂起等服务端返回。服务端跑在 :device 独立进程,Binder 线程池默认 16 条线程处理跨进程请求,正常够用;但如果你在服务端 Stub 方法里做了耗时操作(比如同步等待设备应答),会把 Binder 线程占满,后续客户端调用全部排队超时。所以 sendData 这类方法要么快速返回、要么把耗时动作丢到业务线程。需要纯通知、不关心返回值的地方,可以在 AIDL 方法前加 oneway 关键字让它变异步,调用方立刻返回不阻塞——代价是拿不到返回值,适合纯事件上行。
源文里原本有一张 ASCII 架构图,保留在这里做对照,帮助理解进程边界和数据流向:
┌──────────────────────────────────┐
│ 客户端进程 B │
│ ┌─────────┐ ┌─────────────┐ │
│ │ 业务代码 │───→│ 代理设备对象 │ │
│ └─────────┘ └──────┬──────┘ │
│ │ AIDL │
├────────────────────────┼─────────┤
│ 服务端进程 A │ │
│ ┌─────────┐ ┌──────▼──────┐ │
│ │ 业务代码 │───→│ 真实设备对象 │ │
│ └─────────┘ └─────────────┘ │
│ ┌───────────────────────────┐ │
│ │ 设备共享服务 │ │
│ │ AIDL接口 → 代理到真实设备 │ │
│ └───────────────────────────┘ │
└──────────────────────────────────┘
核心思路再强调一遍:服务端持有真实设备对象,AIDL 接口是设备对象的远程代理。记住这句话,后面看服务端和客户端代码时就不会乱。
三、AIDL 接口怎么设计:控制、事件、内容提供者三层
AIDL 接口不能拍脑袋写,设备要暴露的能力分三类,对应三层接口:控制类(连接、配置、发送)、事件类(监听器回调)、内容提供者类(海量配置参数)。一层一层来。
设备操作接口
IDeviceService 是主接口,负责连接控制、配置、数据发送和设备信息:
// IDeviceService.aidl
interface IDeviceService {
// 连接控制
void connect()
void disconnect()
boolean isConnected()
int connectionStatus()
// 配置
void setAutoReconnect(boolean enabled, long intervalMs)
void setMaxConnectionWaitingTime(int milliSec)
// 数据发送
boolean sendData(in byte[] data)
void sendDifferentialData(in byte[] data)
// 设备信息
DeviceInfo deviceInfo()
DeviceInfo dataLinkDeviceInfo()
// 监听器
void registerDeviceListener(IDeviceListener listener)
void unRegisterDeviceListener(IDeviceListener listener)
// 内容提供者
IContentProvider getContentProvider()
}
注意 in byte[] data 的 in 方向标记——数据从客户端流向服务端,单向即可。AIDL 里参数方向有三种:in 表示客户端传服务端、服务端改了不影响客户端;out 表示服务端填好回传、客户端传入的初值被忽略;inout 双向。方向标错不仅语义乱,还会无谓地跨进程拷贝数据,能写 in 就别偷懒写 inout。另外 DeviceInfo、NmeaData 这些自定义类型必须实现 Parcelable,否则 AIDL 编译器直接报错——这是 AIDL 和Broadcast/文件共享最大的实现差异,类型得自己序列化。连接状态用 connectionStatus() 返回 int(实际是枚举的 ordinal),设备信息用 deviceInfo() 和 dataLinkDeviceInfo() 两个方法区分主设备与数据链设备。
事件监听接口
服务端发生的事件、数据变化、原始字节,都要回传给客户端,靠 IDeviceListener:
// IDeviceListener.aidl
interface IDeviceListener {
void onEvent(int event, in DeviceInfo deviceInfo, String content, int code, in List<Object> params)
void onDataChanged(in NmeaData data)
void onRawData(in byte[] data, int length)
}
onEvent 是通用事件入口,onDataChanged 专门推 NMEA 定位数据,onRawData 把设备吐出的原始字节流直接透传,方便客户端自己做解析。
内容提供者接口:别给每个参数写个方法
设备配置参数特别多——工作模式、卫星系统、UHF 参数等等。如果每个参数都定义一个 AIDL 方法,接口会膨胀到上百个方法,改一次接口所有客户端都要重新编译。更好的做法是定义一个通用的 key-value 内容提供者:
// IContentProvider.aidl
interface IContentProvider {
String getString(String key, String defaultValue)
void setString(String key, String value)
int getInt(String key, int defaultValue)
void setInt(String key, int value)
long getLong(String key, long defaultValue)
void setLong(String key, long value)
float getFloat(String key, float defaultValue)
void setFloat(String key, float value)
boolean getBoolean(String key, boolean defaultValue)
void setBoolean(String key, boolean value)
}
设计优势:无论设备有多少配置参数,AIDL 接口始终只有这几个方法,新增参数只需定义新的 key 常量,接口稳定不受参数膨胀影响。这是整套设计里最值得抄的一点。
四、服务端实现:事件分发、死亡通知、参数序列化
服务端是真正干活的地方。DeviceServerService 持有真实设备对象,把 AIDL 调用转成对真实设备的操作,同时把设备事件分发给所有注册过的客户端监听器。
服务核心
下面这段是服务端骨架,重点看三件事:监听器集合用 Collections.synchronizedList 保证线程安全、原始数据解析器 rawDataParser 怎么转发、事件分发 dispatchEvent 怎么做参数序列化。
class DeviceServerService : Service() {
private val listeners = Collections.synchronizedList(mutableListOf<IDeviceListener>())
private lateinit var realDevice: Device
private lateinit var contentProvider: IContentProviderImpl
// 原始数据转发解析器
private val rawDataParser = object : Parser {
override fun parse(data: ByteArray, length: Int) {
for (listener in listeners) {
try {
listener.onRawData(data, length)
} catch (e: RemoteException) {
// 客户端进程已死,后续会清理
}
}
}
override fun getData(): Data? = null
}
override fun onCreate() {
super.onCreate()
// 获取真实设备对象(由主进程初始化)
realDevice = DeviceManager.getMainDevice()
realDevice.registerEventListener { event, info, content, code, params ->
dispatchEvent(event, info, content, code, params)
}
// 连接后注册原始数据转发
if (realDevice.isConnected()) {
realDevice.dataLinkDevice?.registerParser(rawDataParser)
}
contentProvider = IContentProviderImpl(realDevice.getContent())
}
override fun onBind(intent: Intent): IBinder {
return deviceServiceImp
}
private fun dispatchEvent(
event: DeviceEvent,
info: DeviceInfo,
content: String,
code: Int,
params: Array<Any?>
) {
// 将参数转为可序列化列表
val serializedParams = params.map { param ->
when (param) {
null -> null
is Int, is Long, is Float, is Double, is Boolean -> param
else -> param.toString()
}
}
for (listener in listeners) {
try {
listener.onEvent(event.ordinal, info, content, code, serializedParams)
} catch (e: RemoteException) {
// 客户端进程已死
}
}
}
// AIDL 实现
private val deviceServiceImp = object : IDeviceService.Stub() {
override fun connect() {
realDevice.connect()
}
override fun disconnect() {
realDevice.disconnect()
}
override fun isConnected(): Boolean {
return realDevice.isConnected()
}
override fun connectionStatus(): Int {
return realDevice.connectionStatus().ordinal
}
override fun sendData(data: ByteArray): Boolean {
return realDevice.sendData(data)
}
override fun sendDifferentialData(data: ByteArray) {
realDevice.sendDifferentialData(data)
}
override fun deviceInfo(): DeviceInfo {
return realDevice.getDeviceInfo()
}
override fun registerDeviceListener(listener: IDeviceListener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener)
// 注册死亡通知
try {
listener.asBinder().linkToDeath({
listeners.remove(listener)
}, 0)
} catch (e: RemoteException) {
// 客户端已死
}
}
}
override fun unRegisterDeviceListener(listener: IDeviceListener) {
if (listener != null) {
listeners.remove(listener)
}
}
override fun getContentProvider(): IContentProvider {
return contentProvider
}
// ... 其他方法
}
}
几个关键点串一下:onCreate 里通过 DeviceManager.getMainDevice() 拿到真实设备对象,注册事件监听后转交 dispatchEvent;如果设备已连接,就给数据链设备挂上 rawDataParser 做原始字节转发;deviceServiceImp 是 IDeviceService.Stub() 的实现,每个方法基本就是一行「转发给 realDevice」。
两个实现选择值得点一句。第一,listeners 用 Collections.synchronizedList 而不是普通 ArrayList,是因为事件分发和监听器注册/注销可能发生在不同线程,遍历的时候可能被别的线程改,不加同步会抛 ConcurrentModificationException。第二,rawDataParser 的 getData() 直接返回 null,说明它只关心「收到字节就转发」这一件事,不参与数据对象的生产,纯粹是个透传通道。这两点都是为了在多客户端并发下不把服务端自己搞崩。
内容提供者实现
IContentProviderImpl 包了一层真实设备的 DeviceContent,把 key-value 读写落下去:
class IContentProviderImpl(
private val content: DeviceContent
) : IContentProvider.Stub() {
override fun getString(key: String, defaultValue: String): String {
return content.get(key, defaultValue)
}
override fun setString(key: String, value: String) {
content.set(key, value)
}
override fun getInt(key: String, defaultValue: Int): Int {
return content.get(key, defaultValue)
}
override fun setInt(key: String, value: Int) {
content.set(key, value)
}
// ... 其他类型方法
}
客户端要读配置,不再需要为「工作模式」「卫星系统」各写一个 AIDL 方法,统一走 getString / getInt 加 key 就行。
五、客户端实现:代理设备对象与绑定
客户端要做到一件事:业务代码无感知。它拿到一个 ProxyDevice,用起来和真设备 Device 一模一样,只是内部调用全走 AIDL。
代理设备对象
ProxyDevice 继承自 Device,这样客户端上层代码可以把它当真设备用。ServiceConnection 负责绑定,remoteListener 负责把服务端事件转回本地:
class ProxyDevice(context: Context) : Device() {
private var remoteService: IDeviceService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
remoteService = IDeviceService.Stub.asInterface(service)
// 注册远程监听
remoteService?.registerDeviceListener(remoteListener)
// 通知连接成功
onConnected()
}
override fun onServiceDisconnected(name: ComponentName) {
remoteService = null
onDisconnected()
}
}
// 远程监听器:将服务端事件转发到本地
private val remoteListener = object : IDeviceListener.Stub() {
override fun onEvent(
event: Int,
info: DeviceInfo,
content: String,
code: Int,
params: MutableList<Any>?
) {
// 转发到本地事件分发
dispatchLocalEvent(DeviceEvent.values()[event], info, content, code)
}
override fun onDataChanged(data: NmeaData) {
// 更新本地数据对象
updateLocalData(data)
}
override fun onRawData(data: ByteArray, length: Int) {
// 转发到本地解析管道
onRawDataReceived(data, length)
}
}
fun bindService() {
val intent = Intent("com.example.device.DEVICE_SERVICE")
intent.setPackage("com.example.device")
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
fun unbindService() {
remoteService?.unRegisterDeviceListener(remoteListener)
context.unbindService(serviceConnection)
remoteService = null
}
override fun doConnect() {
remoteService?.connect()
}
override fun doDisconnect() {
remoteService?.disconnect()
}
fun sendData(data: ByteArray): Boolean {
return remoteService?.sendData(data) ?: false
}
fun getContentProvider(): IContentProvider? {
return remoteService?.contentProvider
}
}
注意 bindService 里 intent.setPackage(...) 这一行不能省——Android 5.0 之后隐式 Intent 绑定服务必须显式指定包名,否则会抛 IllegalArgumentException。Context.BIND_AUTO_CREATE 表示服务端进程没起来就拉起它,客户端不用自己先启动服务。doConnect / doDisconnect 是重写父类 Device 的方法,所以上层代码完全不知道自己用的是代理;unbindService 里先 unRegisterDeviceListener 再解绑,顺序反了会在解绑后还回调到已死的本地对象。
还有个时序坑:onServiceConnected 回调发生在 Binder 线程,不是主线程。如果你在 onConnected() 里直接操作 UI,得自己 runOnUiThread 切回去,否则会撞上「不在主线程改 View」的异常。代理对象内部所有对 remoteService 的访问都要判空,因为断连后 remoteService 置 null,此时调用会空指针。
一次跨进程调用到底走了多远
把客户端发一帧差分数据、再收一回原始数据的完整链路画成时序图,你能直观看到 Binder 跨了几次进程边界:
sequenceDiagram
participant C as 客户端业务
participant P as 代理设备对象
participant A as AIDL 接口
participant S as 设备共享服务
participant D as 真实设备对象
C->>P: bindService 绑定
P->>A: asInterface 拿到远程代理
A->>S: registerDeviceListener
C->>P: sendData 差分数据
P->>A: sendData 跨进程
A->>S: sendData
S->>D: 真实发送
D-->>S: 原始数据到达
S-->>A: onRawData 事件
A-->>P: onRawData 回传
P-->>C: 本地解析管道
一次发送,进程边界跨了「客户端→服务端」两趟;一次事件回传,又跨了「服务端→客户端」两趟。这就是 AIDL 双向通信的本质——你每调一个方法,Binder 就帮你穿一次墙。
使用方式
业务侧用起来非常干净,先绑、再读、再发、最后解绑:
// 客户端代码
val device = ProxyDevice(context)
device.bindService()
// 读取设备状态
val provider = device.getContentProvider()
val fixMode = provider?.getString("fix_mode", "未定位")
val satelliteCount = provider?.getInt("satellite_count", 0)
// 发送差分数据
device.sendData(differentialData)
// 断开
device.unbindService()
getContentProvider() 拿到的是远程内容提供者代理,读 fix_mode、satellite_count 这类配置走的是 key-value,和进程内用法一致。
六、跨进程通信的四个关键坑与解法
这套架构真正考验人的不是「能跑」,而是「跑久了不出鬼」。四个坑都是实打实踩出来的。
监听器的死亡通知
客户端进程可能意外崩溃,此时服务端持有的 IDeviceListener 引用就失效了。不清理的话,每次遍历都抛 RemoteException。通过 linkToDeath 注册死亡通知,进程一死自动移除:
override fun registerDeviceListener(listener: IDeviceListener) {
listeners.add(listener)
listener.asBinder().linkToDeath({
// 客户端进程死亡,自动清理
listeners.remove(listener)
}, 0)
}
linkToDeath 的第二个参数是 flags,传 0 即可。它是 Binder 级别的死亡回调,不依赖上层任何心跳机制。
事件参数的序列化
AIDL 只支持基本类型和实现了 Parcelable 的对象。设备事件的参数可能是任意类型(int、double、String、枚举等),必须统一处理成可序列化形式:
// 服务端:将任意参数转为可序列化形式
val serializedParams = params.map { param ->
when (param) {
null -> null
is Int, is Long, is Float, is Double, is Boolean -> param
else -> param.toString() // 其他类型转为字符串
}
}
// 客户端:根据事件类型还原参数
思路是:基本数值类型原样过,其他类型一律 toString() 压成字符串,客户端拿到后按事件类型自己还原。代价是丢了点类型信息,但换来接口稳定。
数据同步频率控制
定位数据每秒可能更新 10-20 次,通过 AIDL 逐帧推送代价太高,Binder 通道会被高频调用堵死。两招并用:
- 数据同步间隔:服务端用
DataSyncManager控制推送频率:
class DataSyncManager(
private val intervalMs: Long = 100L
) {
private var lastSyncTime = 0L
fun shouldSync(): Boolean {
val now = System.currentTimeMillis()
if (now - lastSyncTime >= intervalMs) {
lastSyncTime = now
return true
}
return false
}
}
默认 100ms 一帧,相当于把 10-20 次/秒压到 10 次/秒,既够用又不压垮通道。
- 客户端主动拉取:客户端定时通过
getContentProvider()读最新数据,不被动等推送。高频数据用拉,低频事件用推,分工清晰。
连接状态同步
服务端设备连接状态一变,必须同步到客户端,否则客户端还以为连着。靠 AIDL 事件机制下发:
// 服务端
realDevice.registerEventListener { event, info, content, code, params ->
for (listener in listeners) {
listener.onEvent(event.ordinal, info, content, code, serializedParams)
}
}
// 客户端
override fun onEvent(event: Int, ...) {
when (DeviceEvent.values()[event]) {
DeviceEvent.CONNECTED -> onConnected()
DeviceEvent.DISCONNECTED -> onDisconnected()
DeviceEvent.CONNECT_FAILED -> onConnectFailed()
// ...
}
}
客户端在 onEvent 里把 ordinal 还原成 DeviceEvent 枚举,再驱动自己的连接状态机。
踩坑对照表
把上面四个坑浓缩成一张表,方便出问题直接对照:
| 症状 | 根因 | 解法 |
|---|---|---|
| 服务端遍历监听器抛 RemoteException | 客户端进程已死,引用未清理 | linkToDeath 注册死亡通知自动移除 |
| 事件参数类型不匹配或收不到 | AIDL 只支持基本类型与 Parcelable | 服务端统一序列化为可序列化类型,客户端按事件还原 |
| AIDL 通道卡顿或 ANR | 定位数据每秒 10-20 次逐帧推送 | 服务端控频 DataSyncManager + 客户端主动拉取 |
| 客户端连接状态与实际不一致 | 服务端状态变化未同步 | 通过 onEvent 下发 CONNECTED / DISCONNECTED |
七、Service 声明与权限隔离
代码写完了,清单文件和服务端权限才是「能不能被绑、被谁绑」的最终闸门。
AndroidManifest
服务必须 exported="true" 才能被其他进程绑定,并指定独立进程 :device:
<service
android:name=".DeviceServerService"
android:exported="true"
android:process=":device">
<intent-filter>
<action android:name="com.example.device.DEVICE_SERVICE" />
</intent-filter>
</service>
android:process=":device" 让服务跑在独立进程,和设备连接解耦,主进程崩溃不影响设备持有。注意前面客户端 bindService 里的 setPackage 必须和这里 action 的包名对上。独立进程的意义不只是崩溃隔离——主进程因为内存吃紧被系统回收时,:device 进程还在,设备连接不断,辅助进程和其他 App 照常能拿到数据,等你主进程重启再绑回来即可,不会出现「主进程一重启设备全断」的体验断层。
权限保护
光 exported 不够,得防住别的任意 App 来绑。用 signature 级别权限,只有同一签名的应用才能绑定:
<!-- 声明权限 -->
<permission
android:name="com.example.device.PERMISSION"
android:protectionLevel="signature" />
<!-- 服务端使用权限 -->
<service
android:name=".DeviceServerService"
android:permission="com.example.device.PERMISSION">
...
</service>
<!-- 客户端声明权限 -->
<uses-permission android:name="com.example.device.PERMISSION" />
signature 级别权限的含义是:系统只放行「用同一证书签名」的调用方。同厂多个 App 共用一把签名钥匙,自然能互绑;陌生 App 直接被系统挡在门外,连 Binder 都进不来。
八、四种共享方案横向对比
最后把本方案跟另外两种常见方案拉到一张表上,实时性、双向、流数据、事件、配置读写、复杂度六个维度一目了然:
| 维度 | 本方案 | Broadcast 方案 | ContentProvider 方案 |
|---|---|---|---|
| 实时性 | 高(毫秒级) | 低(百毫秒级) | 低(需轮询) |
| 双向通信 | 支持 | 不支持 | 不支持 |
| 流数据 | 支持 | 受限(1MB) | 不支持 |
| 事件推送 | 支持 | 支持 | 不支持 |
| 配置读写 | 支持 | 不支持 | 支持 |
| 复杂度 | 中 | 低 | 低 |
这张表也回答了一个潜在疑问:为什么不直接用 ContentProvider?它能读写配置,但传不了流数据和事件推送;Broadcast 能推事件,但延迟高、带不动流数据、还单向。只有 AIDL 把实时、双向、流数据三件事同时满足了——代价是复杂度「中」,而这恰恰是前七节要帮你填的坑。
那什么情况下其实不该上这套架构?给你一个粗的选型线:如果只有一个进程用设备、或者设备数据只是偶尔看一眼(几秒一次),那直接进程内持有最省事,AIDL 的复杂度是浪费;如果数据量极大且只读、不需要事件和反向控制,ContentProvider + 文件更轻;只有同时满足「多进程/多 App 都要用」「要实时」「要双向发指令」这三条,AIDL 共享服务才是性价比最高的那一个。换句话说,你先问自己「设备是不是被抢」,再决定要不要做这套排队系统。
小结
- 设备的独占性是物理事实,进程隔离只是把竞争变成排队,共享服务先解决所有权与生命周期,再谈通信。
- 服务端持真实设备、AIDL 做远程代理,客户端用
ProxyDevice无感知共用,Binder 每调一次方法就穿一次进程墙。 - 用 key-value 内容提供者替代「每个参数一个 AIDL 方法」,接口永远只有几个方法,参数膨胀不影响稳定性。
linkToDeath自动清理死亡客户端,DataSyncManager控频 + 客户端拉取,避免高频数据堵死 AIDL 通道。signature级别权限 + 独立:device进程,既保证同厂 App 能互绑,又把陌生调用方挡在 Binder 之外。
系列导航:GIS 系列第 10 篇(共 10 篇),收官。上一篇《多通道设备通信统一抽象》。
你现在的项目里,那个被多进程争抢的设备,是直接各连各的、还是已经有一层共享服务在排队?如果还在各连各的,第一个崩的就是连接那一层——你愿意从哪一步开始改?