第3章 基于 Android BluetoothGatt 的依赖封装实践
3.1 原理:为什么需要隔离与池化
Android 系统提供的 BluetoothGatt 是一个 有状态、单回调注册 的系统级对象。每个 BLE 设备连接对应一个 BluetoothGatt 实例,但其回调 BluetoothGattCallback 在创建时一次性绑定,无法在运行时动态切换。在多设备并发场景下,原始设计存在三个核心痛点:
- 回调冲突:多个业务模块共用同一个 Gatt 对象时,回调会被最后一个注册者覆盖,导致设备状态混乱。
- 资源泄漏:连接建立后若未正确释放,Gatt 对象会持有 Activity / Fragment 引用,造成内存泄漏。
- 线程不可控:系统回调在 binder 线程池中随机触发,直接更新 UI 会引发崩溃,且多线程并发操作 Gatt 方法会导致异常。
依赖封装的核心思路是 代理 + 分发 + 池化:通过 GattProxy 隔离系统 API,通过 CallbackDispatcher 解耦回调归属,通过 ConnectionPool 统一管理连接生命周期,从而让业务层像调用本地服务一样操作远程设备。
3.2 核心接口与抽象设计
隔离的第一步是定义 业务层不依赖系统类型 的回调接口。以下设计将 BluetoothGattCallback 的原始方法缩减为业务关心的三个维度:
// IGattCallback.java — 业务层看到的纯净回调
public interface IGattCallback {
void onConnected(String deviceId);
void onDisconnected(String deviceId, int reason);
void onServicesReady(String deviceId, int status);
void onDataRead(String deviceId, byte[] value, int status);
void onDataWritten(String deviceId, int status);
}
// IGattOperate.java — 业务层发起的操作抽象
public interface IGattOperate {
void connect(String deviceId, int timeoutMs);
void disconnect(String deviceId);
void discoverServices(String deviceId);
void readCharacteristic(String deviceId, String serviceUuid, String charUuid);
void writeCharacteristic(String deviceId, String serviceUuid, String charUuid, byte[] data);
void registerCallback(String deviceId, IGattCallback callback);
void unregisterCallback(String deviceId);
}
| 维度 | 原始 BluetoothGattCallback | 封装后 IGattCallback |
|---|---|---|
| 参数类型 | BluetoothDevice / BluetoothGatt | String deviceId |
| 线程 | binder 线程池(随机) | 调用方指定(默认主线程) |
| 注册方式 | 构造时绑定,不可更换 | 动态注册/注销,多对多 |
| 错误码 | 系统 HIDL 码(-1~257) | 业务语义码(SUCCESS/TIMEOUT/UNKNOWN) |
3.3 连接池设计与实现
连接池的核心是 ConcurrentHashMap + Semaphore,同时管理已连接设备和并发上限。每次业务层请求连接时,连接池先查找是否已有可用代理,没有则创建新的 GattProxy 并登记到池中。
// GattConnectionPool.java — 连接池骨架
public class GattConnectionPool {
private final ConcurrentHashMap<String, GattProxy> activeProxies = new ConcurrentHashMap<>();
private final Semaphore semaphore;
private final int maxConnections;
private final long connectTimeoutMs;
public GattConnectionPool(int maxConnections, long timeoutMs) {
this.maxConnections = maxConnections;
this.connectTimeoutMs = timeoutMs;
this.semaphore = new Semaphore(maxConnections, true);
}
public GattProxy acquire(String deviceId) {
// 1. 先查缓存
GattProxy proxy = activeProxies.get(deviceId);
if (proxy != null && proxy.isAlive()) {
return proxy;
}
// 2. 获取信号量(阻塞)
if (!semaphore.tryAcquire(connectTimeoutMs, TimeUnit.MILLISECONDS)) {
throw new ConnectionPoolException("连接池已满,等待超时");
}
// 3. 创建新代理并注册
proxy = new GattProxy(deviceId, this);
activeProxies.put(deviceId, proxy);
return proxy;
}
public synchronized void release(String deviceId) {
GattProxy proxy = activeProxies.remove(deviceId);
if (proxy != null) {
proxy.close();
semaphore.release();
}
}
public int activeCount() {
return activeProxies.size();
}
}
3.4 GattProxy:系统 API 的线程安全封装
GattProxy 是连接池中的工作单元,它持有 BluetoothGatt 实例,并确保所有操作在 同一线程(Gatt 操作线程) 上串行执行。通过一个专用的 HandlerThread 将系统回调转换为有序消息,避免并发竞态。
// GattProxy.java — 代理封装片段
public class GattProxy {
private final String deviceId;
private BluetoothGatt gatt;
private final HandlerThread gattThread;
private final Handler gattHandler;
private final ConnectionPool pool;
private volatile boolean alive;
public GattProxy(String deviceId, ConnectionPool pool) {
this.deviceId = deviceId;
this.pool = pool;
this.gattThread = new HandlerThread("gatt-" + deviceId);
this.gattThread.start();
this.gattHandler = new Handler(gattThread.getLooper());
this.alive = true;
}
// 所有 Gatt 操作都在 gattHandler 上串行执行
public void connect(Context ctx, BluetoothDevice device, int autoReconnect) {
gattHandler.post(() -> {
gatt = device.connectGatt(ctx, autoReconnect != 0,
new SafeCallbackWrapper(deviceId, pool));
});
}
public void readCharacteristic(String serviceUuid, String charUuid) {
gattHandler.post(() -> {
BluetoothGattService svc = gatt.getService(UUID.fromString(serviceUuid));
if (svc != null) {
svc.getCharacteristic(UUID.fromString(charUuid));
// ... 实际 read 操作
}
});
}
public boolean isAlive() { return alive; }
public void close() {
alive = false;
gattHandler.post(() -> {
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
}
});
gattThread.quitSafely();
}
}
3.5 CallbackDispatcher:回调的路由与线程切换
系统回调首先到达 SafeCallbackWrapper(一个内部实现的 BluetoothGattCallback),它不做业务处理,只做三件事:
- 解析 deviceId:从回调参数中提取 BluetoothDevice 地址,转为字符串 key。
- 按 key 查找注册方:通过
Map<String, IGattCallback>找到对应的业务回调。 - 线程切换:通过
Handler(Looper.getMainLooper())将回调 post 到主线程执行。
// SafeCallbackWrapper.java — 回调安全包装
public class SafeCallbackWrapper extends BluetoothGattCallback {
private final String deviceId;
private final WeakReference<CallbackRegistry> registryRef;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
public SafeCallbackWrapper(String deviceId, CallbackRegistry registry) {
this.deviceId = deviceId;
this.registryRef = new WeakReference<>(registry);
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
CallbackRegistry registry = registryRef.get();
if (registry == null) return;
IGattCallback cb = registry.get(deviceId);
if (cb == null) return;
mainHandler.post(() -> {
if (newState == BluetoothProfile.STATE_CONNECTED) {
cb.onConnected(deviceId);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
cb.onDisconnected(deviceId, status);
}
});
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
CallbackRegistry registry = registryRef.get();
if (registry == null) return;
IGattCallback cb = registry.get(deviceId);
if (cb == null) return;
mainHandler.post(() -> cb.onServicesReady(deviceId, status));
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic ch, int status) {
CallbackRegistry registry = registryRef.get();
if (registry == null) return;
IGattCallback cb = registry.get(deviceId);
if (cb == null) return;
byte[] value = ch.getValue();
mainHandler.post(() -> cb.onDataRead(deviceId, value, status));
}
}
3.6 线程安全全景与锁策略
整个封装体系涉及 三个线程域,每个域有明确的锁策略:
| 线程域 | 包含对象 | 锁策略 | 风险 |
|---|---|---|---|
| 主线程 (UI) | 业务回调 IGattCallback | 无锁(串行消息) | 回调中不要做重量级 IO |
| Gatt 操作线程 | GattProxy 内部 HandlerThread | 串行 Looper,无需显式锁 | 操作队列积压导致延迟 |
| binder 回调线程 | 系统回调入口 | 立即切换,不在该线程停留 | 不可执行任何 Gatt 操作 |
连接池的 ConcurrentHashMap 保证并发读安全,Semaphore 控制准入;release() 方法使用 synchronized 防止同一设备被多次释放导致计数错误。
3.7 调试手段与常用命令
调试封装层是否正确隔离了依赖,需要从三个层面验证:
- 连接池日志:每次 acquire / release 打印池容量变化。
- 回调线程跟踪:在 SafeCallbackWrapper 中打印线程名,确认回调已切换到主线程。
- 系统层嗅探:使用 Bluetooth HCI Snoop 记录原始包。
// 启用 HCI Snoop Log(开发者选项 → 开启蓝牙HCI信息收集日志)
// 拉取日志文件:
adb pull /sdcard/btsnoop_hci.log .
// 使用 Wireshark 打开分析连接建立/断连时序
// 实时查看 Gatt 回调线程:
adb logcat -s BtGatt.GattService --pid $(pid com.android.bluetooth)
// 查看当前连接池状态(自定义日志):
adb logcat -s GattConnectionPool -v threadtime
Thread.currentThread().getName(),若输出 "main" 则切换正确;若输出 "binder:*" 则 SafeCallbackWrapper 的 mainHandler 投递失效。
3.8 常见错误与避坑清单
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
| 连接成功后立即收到断连 | GattProxy 未等待服务发现即返回 | 在 onServicesDiscovered 之后才标记连接就绪 |
| 连接数超过上限仍继续创建 | Semaphore.tryAcquire 超时后未抛出异常 | 捕获超时异常并回调 onError |
| 回调被重复注册导致多次触发 | registerCallback 未做幂等校验 | 注册前先 unregister,或用 CopyOnWriteArraySet |
| 关闭页面后 Gatt 仍占用连接 | 忘记调用 release() | 在 onDestroy 或业务终结时主动释放 |
| 写数据丢失或顺序错乱 | 多个线程同时调用 writeCharacteristic | 所有写操作通过 gattHandler 串行队列 |
| 内存泄漏(Activity 无法回收) | SafeCallbackWrapper 持有 Activity 引用 | 改用 WeakReference + 生命周期感知注册 |
3.9 性能与稳定性优化
- 连接池预热:在 App 启动后创建 1–2 个预占连接,减少首次连接延迟。
- 回调去重合并:短时间内多次 onCharacteristicRead 可合并为一次通知,减少主线程负荷。
- 超时驱逐:在连接池中启动定时扫描,清除超过 30 秒无活动的 GattProxy,避免僵尸连接。
- 降级策略:当连接池满且 acquire 超时时,根据设备优先级决定是否替换已有连接(LRU 淘汰)。
- 批量操作:对同一设备的多个特征值读写使用队列批量提交,减少系统调用次数。
// 连接池超时驱逐示例(定时任务)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
long now = System.currentTimeMillis();
for (Map.Entry<String, GattProxy> entry : activeProxies.entrySet()) {
GattProxy proxy = entry.getValue();
if (now - proxy.getLastActiveTime() > 30_000) {
log.warn("驱逐僵尸连接: " + entry.getKey());
release(entry.getKey());
}
}
}, 10, 10, TimeUnit.SECONDS);
3.10 验证清单
完成封装后,请逐项验证以下场景:
- 单设备连接 → 服务发现 → 读写 → 断连,重复 100 次。
- 5 台不同设备同时发起连接,全部成功且回调互不串扰。
- 在断网(关闭蓝牙)瞬间调用 disconnect,不崩溃、不泄漏。
- 使用 adb 模拟系统低内存,验证 GattProxy 和回调注册表可被正常回收。
- 连接池满后新设备请求 acquire,抛出 ConnectionPoolException 或触发降级。