数据收发与通知回调
本文档详解杰理 Android BLE Demo(ATTConnect)中 BLE 数据的发送、接收与事件通知回调机制,覆盖 BleManager 连接管理层、SendBleDataThread 串行发送线程、BleEventCallbackManager 回调分发器以及 BleEventCallback / OnWriteDataCallback 回调契约,说明数据从应用层到 GATT 特征通道的完整链路。
Purpose and Scope
本页聚焦"数据收发与通知回调"这一核心能力,即 BLE 连接建立后如何:
- 将应用层字节数据按 MTU 分块、串行地写入 GATT 写特征(Write Characteristic);
- 通过通知特征(Notify Characteristic)接收设备上行数据;
- 将扫描、连接、服务发现、收发状态等事件通过统一回调体系分发给 UI 层。
属于本页范围的核心源码:
BleManager—— BLE 连接管理单例(连接、MTU 协商、写数据入口、GATT 回调)SendBleDataThread—— 数据分块与串行发送线程(含重试与超时)BleEventCallbackManager—— 多订阅者事件分发器BleEventCallback/IBleEventCallback/OnWriteDataCallback/IBleOp—— 回调契约与操作抽象
不在本页范围、由兄弟页面覆盖的内容:设备扫描与发现(BluetoothUtil、ScanDeviceInfo、IBtScanCallback)、经典蓝牙(Classic)A2DP/HFP 逻辑、UI 页面布局与业务页面逻辑。这些能力在对应的 Catalog 页面中单独讲解。
Overview
BLE(Bluetooth Low Energy)基于 GATT(Generic Attribute Profile)通信:手机作为 GATT Client,外设作为 GATT Server。一次"数据收发"实际包含两条方向相反、机制不同的通道:
- 下行(手机 → 设备):通过写特征(Write Characteristic)发送。受 BLE 4.0/4.1 单包 20 字节(MTU=23)限制,大包数据必须分块;且底层写操作是异步的,必须等前一个
onCharacteristicWrite回调成功后才能发下一包,否则数据会乱序或丢失。 - 上行(设备 → 手机):通过通知特征(Notify Characteristic)推送。设备主动上报,手机侧在
onCharacteristicChanged回调中接收,属于"异步事件流",与发送方向完全解耦。
杰理 Demo 的封装将这两条通道抽象为:
BleManager:唯一入口。持有 GATT 连接、特征 UUID(服务/写/通知)、MTU 状态,暴露writeDataByBle()等操作,并实现 Android 的BluetoothGattCallback。SendBleDataThread:专门的发送线程,用LinkedBlockingQueue维护待发送任务,保证同一时刻只有一个写操作在途,并提供 8 秒超时与最多 3 次重发。BleEventCallbackManager:事件总线式分发器,把底层 GATT 回调转换为高层语义事件(连接状态、通知开关状态、数据到达、写结果等),统一切换到主线程后广播给所有注册的BleEventCallback。
这种"单例管理器 + 专用发送线程 + 多订阅者回调总线"的架构,解决了三个实际问题:① 系统 GATT 回调线程不可控,必须转主线程;② 异步写并发会导致数据交错;③ 多个 UI 模块需要同时监听同一连接状态。
Architecture
flowchart TD
subgraph sg_App["应用层 (UI / 业务模块)"]
UI["Activity / Fragment"]
Cb1["BleEventCallback 实现 A"]
Cb2["BleEventCallback 实现 B"]
WrCb["OnWriteDataCallback"]
end
subgraph sg_BleLib["ble 工具包 (com.jieli.bt.att.tool.ble)"]
BM["BleManager<br/>(单例,实现 BluetoothGattCallback)"]
SendThread["SendBleDataThread<br/>(串行发送线程)"]
CbMgr["BleEventCallbackManager<br/>(事件分发器)"]
IOp["IBleOp<br/>(操作抽象)"]
end
subgraph sg_Android["Android 系统 BLE 栈"]
GATT["BluetoothGatt"]
CHAR_W["Write Characteristic"]
CHAR_N["Notify Characteristic"]
end
UI -->|"registerBleEventCallback"| CbMgr
CbMgr -->|"广播事件(主线程)"| Cb1
CbMgr -->|"广播事件(主线程)"| Cb2
UI -->|"addSendTask + OnWriteDataCallback"| SendThread
SendThread -->|"writeDataByBle (IBleOp)"| BM
SendThread -->|"onBleResult(成功/失败)"| WrCb
BM -->|"gatt.writeCharacteristic"| GATT
GATT -->|"onCharacteristicWrite"| BM
GATT -->|"onCharacteristicChanged"| BM
BM -->|"onBleDataNotification / onBleWriteStatus"| CbMgr
GATT --> CHAR_W
GATT --> CHAR_N
BM -.->|"实现"| IOp
IOp -.->|"writeDataByBle / getBleMtu"| SendThread
架构解读:
- BleManager 是整个数据收发的枢纽:它实现
BluetoothGattCallback直接接收 Android 系统层的写结果与通知数据,同时实现IBleOp接口供发送线程调用。单例通过双重检查锁创建(getInstance()),生命周期与应用进程一致。 - SendBleDataThread 不直接接触系统 API,只依赖
IBleOp抽象(writeDataByBle()、getBleMtu()),因此发送逻辑可独立测试,也便于替换底层实现。 - BleEventCallbackManager 继承
BleEventCallback抽象类,内部持有多个订阅者列表;BleManager只调用这一个管理器,管理器负责把事件复制分发到所有订阅者并保证主线程回调。 - 上下行通道完全分离:下行走"发送线程 → 写特征 → onCharacteristicWrite",上行走"通知特征 → onCharacteristicChanged → onBleDataNotification",二者互不阻塞。
相关源码:
数据发送链路(下行)
1. 特征与配置常量
发送通道的关键参数在 BleManager 中集中定义:服务 UUID、写特征 UUID、通知特征 UUID 均来自 Config(可在应用配置中修改以适配不同外设),通知描述符使用标准 CCCD(Client Characteristic Configuration Descriptor,0x2902):
//BLE服务UUID
public final static UUID BLE_UUID_SERVICE = Config.INSTANCE.getBLE_SERVICE_UUID();
//BLE的写特征UUID
public final static UUID BLE_UUID_WRITE = Config.INSTANCE.getBLE_WRITE_UUID();
//BLE的通知特征UUID
public final static UUID BLE_UUID_NOTIFICATION = Config.INSTANCE.getBLE_NOTIFY_UUID();
//BLE的通知特征的描述符UUID
public final static UUID BLE_UUID_NOTIFICATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB");
Source: BleManager.java
设计意图:把 UUID 收敛为静态常量并由 Config 提供,既避免在业务代码中散落魔法字符串,也允许同一套库适配不同厂商的 GATT 服务定义。
2. 入口:addSendTask 分块入队
应用层发送数据的入口是 SendBleDataThread.addSendTask()。它接收完整的字节数组,先查询当前协商的 MTU(mBleManager.getBleMtu()),再把数据按 MTU 大小切块,逐块封装为 BleSendTask 放入 LinkedBlockingQueue:
public boolean addSendTask(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) {
if (null == mBleManager || null == gatt || null == serviceUUID || null == characteristicUUID || null == data || data.length == 0) {
return false;
}
int mtu = mBleManager.getBleMtu();
JL_Log.d(TAG, "addSendTask", "mtu : " + mtu);
int dataLen = data.length;
int blockCount = dataLen / mtu;
boolean ret = false;
for (int i = 0; i < blockCount; i++) {
byte[] mBlockData = new byte[mtu];
System.arraycopy(data, i * mtu, mBlockData, 0, mBlockData.length);
ret = addSendData(gatt, serviceUUID, characteristicUUID, mBlockData, callback);
}
if (0 != dataLen % mtu) {
byte[] noBlockData = new byte[dataLen % mtu];
System.arraycopy(data, dataLen - dataLen % mtu, noBlockData, 0, noBlockData.length);
ret = addSendData(gatt, serviceUUID, characteristicUUID, noBlockData, callback);
}
return ret;
}
Source: SendBleDataThread.java
要点分析:
- 空参数防御:gatt、UUID、data 任一为空或 data 长度为 0 直接返回
false,杜绝无效任务进入队列。 - 整除与余数处理:先发送
dataLen / mtu个整块,再把余数dataLen % mtu作为最后一块发送。注意这里没有处理mtu == 0的除零风险,实际运行中 MTU 由BleManager保证至少为 20(见 MTU 超时兜底逻辑),因此是安全的。 - 返回语义:返回的是"入队是否成功"而非"发送成功",真正的结果通过
OnWriteDataCallback异步上报。
addSendData 内部还处理了"线程休眠中入队需唤醒"的协作逻辑:若发送线程因队列为空而 wait(),入队成功后会 notify() 唤醒它继续取任务:
private boolean addSendData(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) {
boolean ret = false;
if (isDataSend) {
BleSendTask sendTask = new BleSendTask(gatt, serviceUUID, characteristicUUID, data, callback);
try {
mQueue.put(sendTask);
ret = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ret && isThreadWaiting && !isWaitingForCallback) {
isThreadWaiting = false;
synchronized (mQueue) {
mQueue.notify();
}
}
}
return ret;
}
Source: SendBleDataThread.java
3. 串行发送主循环:等待、写入、超时、重试
SendBleDataThread 继承 Thread,其 run() 是发送的核心状态机。整体逻辑:队列空则等待;队列非空则取出队首任务调用 writeDataByBle() 发起写,然后阻塞等待底层写回调(最长 8 秒),根据回调结果决定成功、重发或失败:
@Override
public void run() {
JL_Log.d(TAG, "send ble data thread is started.");
if (mListener != null) {
mListener.onStart(getId(), getName());
}
if (mBleManager != null) {
synchronized (mQueue) {
while (isDataSend) {
mCurrentTask = null;
isThreadWaiting = false;
isWaitingForCallback = false;
if (mQueue.isEmpty()) {
isThreadWaiting = true;
JL_Log.d(TAG, "queue is empty, so waiting for data");
try {
mQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
mCurrentTask = mQueue.peek();
if (mCurrentTask != null) {
isWaitingForCallback = mBleManager.writeDataByBle(mCurrentTask.mGatt, mCurrentTask.getServiceUUID(),
mCurrentTask.getCharacteristicUUID(), mCurrentTask.getData());
if (isWaitingForCallback) {
try {
mQueue.wait(BleManager.SEND_DATA_MAX_TIMEOUT);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
mCurrentTask.setStatus(-1);
}
JL_Log.d(TAG, "data send ret :" + mCurrentTask.getStatus());
if (mCurrentTask.getStatus() != BluetoothGatt.GATT_SUCCESS) { //发送失败
retryNum++;
if (retryNum >= 3) { //重发次数超过限制
callbackResult(mCurrentTask, false);
mQueue.clear();
} else {
if (mCurrentTask.getStatus() != -1) {
mCurrentTask.setStatus(-1);
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
continue;
}
} else { //发送成功
callbackResult(mCurrentTask, true);
}
}
retryNum = 0;
if (!mQueue.isEmpty()) mQueue.poll();
}
}
}
isWaitingForCallback = false;
isThreadWaiting = false;
mQueue.clear();
if (mListener != null) {
mListener.onEnd(getId(), getName());
}
JL_Log.d(TAG, "send ble data thread exit.");
}
}
Source: SendBleDataThread.java
状态机关键点:
- wait/notify 协作:整个循环持有
synchronized (mQueue),队列空时mQueue.wait()挂起线程;addSendData/wakeupSendThread入队或收到写回调后notify()唤醒。isWaitingForCallback区分"等新任务"和"等写回调"两种等待,避免唤醒语义混乱。 - 写回调如何回到线程:
writeDataByBle()由BleManager实现,底层写完成(onCharacteristicWrite)后,BleManager会调用发送线程的wakeupSendThread(task)——该方法将"正在等待回调"的线程notifyAll()/notify(),并把回调中携带的状态写入mCurrentTask.setStatus(...)。 - 超时:
mQueue.wait(SEND_DATA_MAX_TIMEOUT),即 8 秒(BleManager.SEND_DATA_MAX_TIMEOUT = 8000)。若 8 秒内没有收到写回调,任务状态停留在初始值-1,走失败分支。 - 重试策略:失败后
retryNum++;retryNum >= 3时放弃——回调onBleResult(..., false)并mQueue.clear()清空剩余任务(防止后续任务继续堆积在一个已断连的链路上);否则continue重发同一任务(保持在队首,peek不弹出)。 - 成功:
callbackResult(mCurrentTask, true)后poll()弹出任务,retryNum归零。
4. 发送结果回调契约
每个分块任务都携带同一个 OnWriteDataCallback,发送结果通过 callbackResult 统一上报:
private void callbackResult(BleSendTask task, boolean result) {
if (task != null && task.getCallback() != null) {
if (task.getBleGatt() == null) return;
task.getCallback().onBleResult(task.getBleGatt().getDevice(), task.getServiceUUID(),
task.getCharacteristicUUID(), result, task.getData());
} else {
JL_Log.i(TAG, "getCallback is null.");
}
}
Source: SendBleDataThread.java
OnWriteDataCallback 接口定义在 interfaces/OnWriteDataCallback.java,其唯一方法 onBleResult(BluetoothDevice device, UUID serviceUuid, UUID characteristicUuid, boolean result, byte[] data) 把"设备 + 特征 + 结果 + 原始数据"一并回传,业务层可据此精确知道哪一包数据发送成败。
BleSendTask 是发送队列的载体(SendBleDataThread 内部静态类),字段包括:mGatt(目标连接)、serviceUUID、characteristicUUID、data(单块数据)、status(默认 -1,写回调写入 GATT_SUCCESS 或错误码)、mCallback。status 初始 -1 的设计让"超时未回调"与"回调失败"都能落入失败分支。
通知回调机制(上行)
1. 事件回调抽象与接口
上行通道以事件驱动:设备通过通知特征主动上报数据,同时连接、服务发现、通知开关等状态变化也会产生事件。所有事件先被规约到 IBleEventCallback 接口,BleEventCallback 抽象类提供空实现,业务层只需覆写关心的方法:
public abstract class BleEventCallback implements IBleEventCallback {
@Override
public void onAdapterChange(boolean bEnabled) { }
@Override
public void onDiscoveryState(boolean bStart) { }
@Override
public void onDiscoveryDevice(ScanDeviceInfo bleScanMessage) { }
@Override
public void onDiscoveryFail(int code, String message) { }
@Override
public void onBleConnection(BluetoothDevice device, int status) { }
@Override
public void onBleServiceDiscovery(BluetoothDevice device, int status, List<BluetoothGattService> services) { }
@Override
public void onBleNotificationStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicUuid, int status) { }
@Override
public void onBleDataBlockChanged(BluetoothDevice device, int block, int status) { }
@Override
public void onBleDataNotification(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data) { }
@Override
public void onBleWriteStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data, int status) { }
@Override
public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout, int status) { }
}
Source: BleEventCallback.java
设计意图:抽象类 + 空实现(而非强制全部实现的接口)是典型的"适配器模式"变体——UI 层往往只关心数据到达和连接状态两个事件,若实现完整接口将被迫写出大量空方法。IBleEventCallback 保留完整接口用于内部强约束,BleEventCallback 提供友好基类给业务层继承。
与"数据收发"最相关的三个事件:
| 事件方法 | 触发时机 | 业务含义 |
|---|---|---|
onBleDataNotification(device, serviceUuid, characteristicsUuid, data) | 设备通过通知特征上报数据 | 上行数据的最终入口 |
onBleWriteStatus(device, serviceUuid, characteristicsUuid, data, status) | 写特征回调完成 | 单包写入结果(与 OnWriteDataCallback 双通道冗余上报) |
onBleNotificationStatus(device, serviceUuid, characteristicUuid, status) | 通知开关写入 CCCD 完成 | 确认"订阅通知"是否成功 |
2. 多订阅者分发:BleEventCallbackManager
BleManager 内部持有一个 BleEventCallbackManager,所有底层事件先汇聚到这里,再广播给所有注册者。它继承 BleEventCallback,因此对 BleManager 而言它就是一个普通回调对象:
public class BleEventCallbackManager extends BleEventCallback {
private final ArrayList<BleEventCallback> mCallbacks = new ArrayList<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
public void registerBleEventCallback(BleEventCallback callback) {
if (callback != null && !mCallbacks.contains(callback)) {
mCallbacks.add(callback);
}
}
public void unregisterBleEventCallback(BleEventCallback callback) {
if (callback != null && !mCallbacks.isEmpty()) {
mCallbacks.remove(callback);
}
}
public void release() {
mCallbacks.clear();
mHandler.removeCallbacksAndMessages(null);
}
...
}
Source: BleEventCallbackManager.java
关键设计:
- 去重注册:
contains检查防止同一订阅者重复注册导致事件被回调多次。 - release 清理:清空订阅者并移除主线程 Handler 上所有待执行的回调任务,用于释放资源、防止内存泄漏(Activity 销毁时若忘记反注册,回调仍会持有一个已销毁的实例)。
- 主线程切换:所有事件统一通过
callbackBleEvent投递到主线程:
private void callbackBleEvent(BleEventCallbackImpl impl) {
if (null == impl) return;
OnBleEventRunnable runnable = new OnBleEventRunnable(impl);
if (Thread.currentThread().getId() == Looper.getMainLooper().getThread().getId()) {
runnable.run();
} else {
mHandler.post(runnable);
}
}
private class OnBleEventRunnable implements Runnable {
private final BleEventCallbackImpl mImpl;
public OnBleEventRunnable(BleEventCallbackImpl impl) {
mImpl = impl;
}
@Override
public void run() {
if (!mCallbacks.isEmpty() && mImpl != null) {
for (BleEventCallback callback : new ArrayList<>(mCallbacks)) {
if (callback != null) {
mImpl.onCallback(callback);
}
}
}
}
}
Source: BleEventCallbackManager.java
三点值得注意:
- 已在线程上则直接执行:
callbackBleEvent先判断当前线程是否为主线程;GATT 回调可能已发生在主线程(部分系统版本如此),此时直接run()避免 Handler 入队延迟。 - 遍历快照:
new ArrayList<>(mCallbacks)复制列表再遍历,防止回调过程中订阅者注册/注销(mCallbacks结构性修改)导致ConcurrentModificationException。 BleEventCallbackImpl函数式接口:每个事件方法通过 Lambda(如callback -> callback.onBleDataNotification(...))描述"对每个订阅者做什么",避免为 11 个事件各写一个 Runnable 类。
3. 上行数据流:从系统回调到业务层
设备上报数据的完整链路为:
- 系统 BLE 栈收到通知特征数据 →
BluetoothGattCallback.onCharacteristicChanged(gatt, characteristic); BleManager(实现该回调)取出characteristic.getValue(),调用mCallbackManager.onBleDataNotification(device, serviceUuid, characteristicsUuid, data);BleEventCallbackManager把事件切换到主线程,广播给所有已注册的BleEventCallback;- UI 层在
onBleDataNotification中解析数据(例如CHexConver转十六进制显示)。
发送方向的写回调则走 onCharacteristicWrite → BleManager 更新任务状态并 wakeupSendThread → 发送线程判定成功/失败 → OnWriteDataCallback.onBleResult,同时 BleManager 也会调用 mCallbackManager.onBleWriteStatus(...) 做事件广播(双通道冗余,UI 与数据层各取所需)。
Core Flow(核心时序)
sequenceDiagram
participant UI as UI / 业务层
participant ST as SendBleDataThread
participant BM as BleManager
participant CB as BleEventCallbackManager
participant GATT as BluetoothGatt
participant DEV as 设备 (GATT Server)
Note over UI,DEV: 下行:分块串行发送
UI->>ST: addSendTask(gatt, uuid, data, callback)
ST->>ST: 按 MTU 分块,逐块入队 LinkedBlockingQueue
ST->>BM: writeDataByBle(gatt, service, char, block)
BM->>GATT: gatt.writeCharacteristic(characteristic)
GATT->>DEV: 写入 Write Characteristic
DEV-->>GATT: 写确认
GATT-->>BM: onCharacteristicWrite(gatt, char, status)
BM->>ST: wakeupSendThread(task) / setStatus(status)
ST->>ST: 状态==GATT_SUCCESS ? 成功 : 重试/失败
ST-->>UI: onBleResult(device, uuid, result, data)
Note over UI,DEV: 上行:设备主动通知
DEV-->>GATT: Notify Characteristic 上报数据
GATT-->>BM: onCharacteristicChanged(gatt, char)
BM->>CB: onBleDataNotification(device, uuid, data)
CB->>CB: 切换到主线程,遍历订阅者快照
CB-->>UI: onBleDataNotification(device, uuid, data)
时序解读:
- 下行是严格串行的:一个分块在途时发送线程阻塞在
mQueue.wait(SEND_DATA_MAX_TIMEOUT),直到写回调唤醒它才继续下一块,天然避免 BLE 写并发交错。 - 上行是事件驱动的:设备何时上报、上报多少包,手机侧无法预测;
onCharacteristicChanged每次触发都会立即分发,不经过发送队列。 - 两条链路共用
BleManager但互不阻塞:发送线程阻塞的是它自己的等待,不影响onCharacteristicChanged在主线程(或系统 Binder 线程)触发回调。
Usage Examples
示例 1:注册事件回调并接收上行数据
业务层(如 Activity)继承 BleEventCallback,只覆写关心的 onBleDataNotification 与 onBleConnection,然后注册到 BleManager 的回调管理器(通过 BleManager 对外暴露的注册入口,内部即 BleEventCallbackManager):
private final BleEventCallback mBleEventCallback = new BleEventCallback() {
@Override
public void onBleConnection(BluetoothDevice device, int status) {
// 连接状态变化:BluetoothProfile.STATE_CONNECTED / STATE_DISCONNECTED
}
@Override
public void onBleDataNotification(BluetoothDevice device, UUID serviceUuid,
UUID characteristicsUuid, byte[] data) {
// 设备上报的数据到达,解析并刷新 UI
String hex = CHexConver.bytesToHexString(data); // 十六进制展示
}
};
@Override
protected void onStart() {
super.onStart();
BleManager.getInstance().registerBleEventCallback(mBleEventCallback);
}
@Override
protected void onStop() {
super.onStop();
BleManager.getInstance().unregisterBleEventCallback(mBleEventCallback);
}
Source(回调抽象类定义): BleEventCallback.java
示例 2:发送数据并获取逐包结果
通过 OnWriteDataCallback 获取每个分块的发送结果。大包数据由 SendBleDataThread 自动按 MTU 切分,业务层只需提供完整字节数组:
byte[] payload = CHexConver.hexStringToBytes("01 02 03 ..."); // 完整业务数据包
BleManager.getInstance().sendDataToDevice(
gatt, BleManager.BLE_UUID_SERVICE, BleManager.BLE_UUID_WRITE, payload,
new OnWriteDataCallback() {
@Override
public void onBleResult(BluetoothDevice device, UUID serviceUuid,
UUID characteristicUuid, boolean result, byte[] data) {
if (result) {
// 该分块发送成功
} else {
// 该分块重试 3 次后仍失败(或超时)
}
}
});
Source(分块与回调契约): SendBleDataThread.java、SendBleDataThread.java
示例 3:发送线程生命周期监听
OnThreadStateListener 用于感知发送线程的启动与退出(例如在发送结束时恢复按钮可点击状态):
SendBleDataThread thread = new SendBleDataThread(mBleManager, new OnThreadStateListener() {
@Override
public void onStart(long threadId, String threadName) {
// 线程启动:禁用发送按钮
}
@Override
public void onEnd(long threadId, String threadName) {
// 线程退出:恢复 UI 状态
}
});
thread.start(); // 启动后进入等待任务状态(isDataSend = true)
Configuration Options
数据收发相关的可配置项集中在 BleManager 常量与 Config 中:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
BLE_UUID_SERVICE | UUID | Config.getBLE_SERVICE_UUID() | GATT 服务 UUID,需与设备固件一致 |
BLE_UUID_WRITE | UUID | Config.getBLE_WRITE_UUID() | 写特征 UUID,下行数据通道 |
BLE_UUID_NOTIFICATION | UUID | Config.getBLE_NOTIFY_UUID() | 通知特征 UUID,上行数据通道 |
BLE_UUID_NOTIFICATION_DESCRIPTOR | UUID | 00002902-...-00805F9B34FB | CCCD 描述符(标准值) |
SEND_DATA_MAX_TIMEOUT | int | 8000 ms | 单块发送等待写回调的最大超时 |
SCAN_BLE_TIMEOUT | int | 12 s | 扫描超时(间接影响收发链路可用性) |
CONNECT_BLE_TIMEOUT | int | 40 s | 连接超时 |
MIN_CONNECT_TIME | int | 8 s | 连接最小时间阈值 |
| MTU | int | 协商值,超时兜底 20 | 分块大小依据;MTU 协商失败时退回 20 字节 |
Source: BleManager.java、BleManager.java
API Reference
BleManager(单例)
| 方法 | 说明 |
|---|---|
getInstance() | 双重检查锁单例,持有应用 Context,初始化蓝牙适配器与扫描器 |
registerBleEventCallback(BleEventCallback cb) | 注册事件订阅者(转发给 BleEventCallbackManager) |
unregisterBleEventCallback(BleEventCallback cb) | 注销事件订阅者 |
writeDataByBle(gatt, serviceUuid, charUuid, data): boolean | 发起一次 GATT 写(由发送线程调用),返回是否等待回调 |
getBleMtu(): int | 返回当前协商 MTU,供分块使用 |
SendBleDataThread
| 方法 | 说明 |
|---|---|
addSendTask(gatt, serviceUuid, charUuid, data, callback): boolean | 按 MTU 分块入队,返回是否全部入队成功 |
start() | 置 isDataSend = true 后启动线程 |
stopThread() | 置 isDataSend = false 并唤醒线程,线程退出并清空队列 |
isRunning(): boolean | 线程是否处于运行状态 |
wakeupSendThread(BleSendTask task) | 写回调到达时唤醒等待中的线程并写入状态 |
Source: SendBleDataThread.java
OnWriteDataCallback(发送结果回调)
void onBleResult(BluetoothDevice device, UUID serviceUuid,
UUID characteristicUuid, boolean result, byte[] data);
- device:目标设备
- serviceUuid / characteristicUuid:写入的服务与特征
- result:
true表示发送成功(写回调返回GATT_SUCCESS);false表示重试 3 次后仍失败或超时 - data:本次分块的实际字节数据
BleEventCallback 关键回调(上行)
onBleDataNotification(device, serviceUuid, characteristicsUuid, data):设备通知数据到达onBleWriteStatus(device, serviceUuid, characteristicsUuid, data, status):单包写状态事件onBleNotificationStatus(device, serviceUuid, characteristicUuid, status):通知开关(CCCD)写入结果onBleConnection(device, status):连接状态变化(BluetoothProfile.STATE_CONNECTED/STATE_DISCONNECTED)
Failure Modes、边界情况与并发
1. 发送失败与重试耗尽
当写回调返回非 GATT_SUCCESS 或 8 秒超时未收到回调时,SendBleDataThread 进入重试分支:retryNum++,达到 3 次后判定失败,回调 onBleResult(..., false) 并 mQueue.clear() 清空剩余任务。
关键行为与原因:
- 重试期间任务不弹出(
peek而非poll),保证重发的是同一包数据; - 失败后清空队列是刻意的"快速失败":链路已不可用(例如设备掉线),继续发送后续任务只会白白等待 8 秒超时;
- 每次失败分支会
sleep(10)短暂让出 CPU,避免在设备侧未就绪时高频重写。
2. 超时兜底逻辑(BleManager)
BleManager 内部 Handler 管理多个超时消息,与收发链路直接相关的有:
MSG_NOTIFY_BLE_TIMEOUT(0x1013):开启通知后超时未收到响应 → 直接断开连接,防止设备"假连接";MSG_CHANGE_BLE_MTU_TIMEOUT(0x1014):MTU 协商超时 →bleDevice.setMtu(20)兜底到 20 字节,再继续连接流程。该兜底保证getBleMtu()永远返回 ≥ 20,从而避免发送线程dataLen / mtu除零;MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT(0x1015):服务发现回调超时 → 若已拿到服务列表则手动补发onServicesDiscovered,否则置needReconnect并断开,等待重连。
Source: BleManager.java
3. 并发与线程安全
- 发送串行化:
SendBleDataThread.run()全程持有synchronized (mQueue),wait/notify 都在该监视器上;任意时刻只有一个写操作在途,杜绝了"多线程同时 writeCharacteristic"导致的 Android 系统层异常(onCharacteristicWrite与onCharacteristicChanged回调交错、数据乱序)。 - 单例线程安全:
BleManager.instance为volatile且getInstance()使用双重检查锁;mUsingDevice亦为volatile,保证跨线程可见性。 - 回调线程切换:
BleEventCallbackManager.callbackBleEvent判断当前线程,非主线程一律mHandler.post,保证业务回调总在主线程执行,UI 可直接更新控件。 - 遍历快照:事件广播遍历
new ArrayList<>(mCallbacks)副本,回调中注册/注销不会抛ConcurrentModificationException。
4. 边界情况
- 空数据:
addSendTask对data.length == 0直接返回false,不产生空分块; - MTU 余数:
dataLen % mtu != 0时最后一块为余数长度(≤ MTU-3 的有效载荷上限由系统保证); - 队列满:
LinkedBlockingQueue无界,put永不阻塞,但极端情况下大量失败任务会被mQueue.clear()兜底清理; - 通知未开启:若设备未先订阅通知(CCCD 未写入),
onCharacteristicChanged不会触发,上行数据不会到达——订阅成功与否通过onBleNotificationStatus事件反馈。
Performance 与运维建议
- 吞吐上限:单块发送 = 1 次写 + 1 次写回调 + 唤醒,Android 常规 BLE 写吞吐约 2~5 KB/s(MTU=23 时更低)。提升吞吐应先协商大 MTU(如 247),再配合连接参数更新(
onConnectionUpdated事件可观察 interval/latency/timeout)。 - 超时参数权衡:
SEND_DATA_MAX_TIMEOUT = 8s对高负载链路偏保守——若设备处理慢,8 秒内未回写回调会触发重发,可能造成重复数据。业务层需自行做幂等/去重,或按需调大该常量。 - 发送线程单例化:发送线程应在连接建立后启动、断开时
stopThread(),避免长时间空转;线程退出时自动mQueue.clear()防止内存滞留。 - 日志:收发链路大量使用
JL_Log.d/i(TAG 为BleManager/SendBleDataThread),线上定位丢包问题可优先检索"data send ret"、"MSG_*_TIMEOUT"日志。
Extension Points(扩展点)
- 替换底层实现:
SendBleDataThread只依赖IBleOp(writeDataByBle/getBleMtu),可注入自定义实现(例如模拟器、日志回放、多设备分发的适配层)而不改发送逻辑; - 新增事件类型:在
IBleEventCallback增加方法并在BleEventCallback补空实现 →BleEventCallbackManager重写该方法做callbackBleEvent分发 →BleManager在对应系统回调处调用。三处修改即可扩展事件总线; - 自定义分块策略:
addSendTask的 MTU 分块逻辑是线程内私有实现,若需自定义协议头/分包格式,可在调用前自行切分后逐段addSendTask(每段 ≤ MTU),或扩展BleSendTask增加协议字段; - 发送线程状态监听:实现
OnThreadStateListener可感知线程启停,用于 UI 状态机(发送中禁用按钮、结束恢复)。
Tests(测试现状)
仓库未发现针对 SendBleDataThread / BleEventCallbackManager 的独立单元测试文件(按 ble 目录扫描,仅实现类与接口,无 test 目录下的对应用例)。发送队列与回调分发逻辑的正确性目前依赖真机 BLE 联调验证。建议后续补充:
SendBleDataThread分块正确性(整除 / 余数 / 空数据)单测;- 重试耗尽与超时路径(Mock
IBleOp.writeDataByBle返回 false / 不回调); BleEventCallbackManager注册去重、注销、遍历中修改订阅者列表的健壮性测试。
Related Links
- 数据收发与通知回调(本页)
- 设备扫描与发现(
BluetoothUtil、ScanDeviceInfo、IBtScanCallback)—— 连接前的设备获取,见扫描相关 Catalog 页面 - 连接管理与状态机(
BleManager连接流程、重连、MTU 协商)—— 见连接管理 Catalog 页面 - 核心源码: