杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 仓库简介与示例构成
    • 支持的平台与协议
  • 快速开始

    • 导入工程与编译运行
    • 蓝牙权限配置
  • 应用架构

    • 工程结构与模块分层
    • 核心类与回调接口
  • 核心功能

    • 蓝牙设备扫描
    • ATT 设备连接与断开管理
    • 数据收发与通知回调
  • 配置与调试

    • 协议配置常量
    • 日志系统与调试指南
  • 界面与交互

    • 设备扫描与连接界面
    • 设备详情与设置界面

数据收发与通知回调

本文档详解杰理 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。一次"数据收发"实际包含两条方向相反、机制不同的通道:

  1. 下行(手机 → 设备):通过写特征(Write Characteristic)发送。受 BLE 4.0/4.1 单包 20 字节(MTU=23)限制,大包数据必须分块;且底层写操作是异步的,必须等前一个 onCharacteristicWrite 回调成功后才能发下一包,否则数据会乱序或丢失。
  2. 上行(设备 → 手机):通过通知特征(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",二者互不阻塞。

相关源码:

  • BleManager.java
  • SendBleDataThread.java
  • BleEventCallbackManager.java
  • BleEventCallback.java

数据发送链路(下行)

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

状态机关键点:

  1. wait/notify 协作:整个循环持有 synchronized (mQueue),队列空时 mQueue.wait() 挂起线程;addSendData/wakeupSendThread 入队或收到写回调后 notify() 唤醒。isWaitingForCallback 区分"等新任务"和"等写回调"两种等待,避免唤醒语义混乱。
  2. 写回调如何回到线程:writeDataByBle() 由 BleManager 实现,底层写完成(onCharacteristicWrite)后,BleManager 会调用发送线程的 wakeupSendThread(task)——该方法将"正在等待回调"的线程 notifyAll()/notify(),并把回调中携带的状态写入 mCurrentTask.setStatus(...)。
  3. 超时:mQueue.wait(SEND_DATA_MAX_TIMEOUT),即 8 秒(BleManager.SEND_DATA_MAX_TIMEOUT = 8000)。若 8 秒内没有收到写回调,任务状态停留在初始值 -1,走失败分支。
  4. 重试策略:失败后 retryNum++;retryNum >= 3 时放弃——回调 onBleResult(..., false) 并 mQueue.clear() 清空剩余任务(防止后续任务继续堆积在一个已断连的链路上);否则 continue 重发同一任务(保持在队首,peek 不弹出)。
  5. 成功: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

三点值得注意:

  1. 已在线程上则直接执行:callbackBleEvent 先判断当前线程是否为主线程;GATT 回调可能已发生在主线程(部分系统版本如此),此时直接 run() 避免 Handler 入队延迟。
  2. 遍历快照:new ArrayList<>(mCallbacks) 复制列表再遍历,防止回调过程中订阅者注册/注销(mCallbacks 结构性修改)导致 ConcurrentModificationException。
  3. BleEventCallbackImpl 函数式接口:每个事件方法通过 Lambda(如 callback -> callback.onBleDataNotification(...))描述"对每个订阅者做什么",避免为 11 个事件各写一个 Runnable 类。

3. 上行数据流:从系统回调到业务层

设备上报数据的完整链路为:

  1. 系统 BLE 栈收到通知特征数据 → BluetoothGattCallback.onCharacteristicChanged(gatt, characteristic);
  2. BleManager(实现该回调)取出 characteristic.getValue(),调用 mCallbackManager.onBleDataNotification(device, serviceUuid, characteristicsUuid, data);
  3. BleEventCallbackManager 把事件切换到主线程,广播给所有已注册的 BleEventCallback;
  4. 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)

Source: SendBleDataThread.java、SendBleDataThread.java

Configuration Options

数据收发相关的可配置项集中在 BleManager 常量与 Config 中:

配置项类型默认值说明
BLE_UUID_SERVICEUUIDConfig.getBLE_SERVICE_UUID()GATT 服务 UUID,需与设备固件一致
BLE_UUID_WRITEUUIDConfig.getBLE_WRITE_UUID()写特征 UUID,下行数据通道
BLE_UUID_NOTIFICATIONUUIDConfig.getBLE_NOTIFY_UUID()通知特征 UUID,上行数据通道
BLE_UUID_NOTIFICATION_DESCRIPTORUUID00002902-...-00805F9B34FBCCCD 描述符(标准值)
SEND_DATA_MAX_TIMEOUTint8000 ms单块发送等待写回调的最大超时
SCAN_BLE_TIMEOUTint12 s扫描超时(间接影响收发链路可用性)
CONNECT_BLE_TIMEOUTint40 s连接超时
MIN_CONNECT_TIMEint8 s连接最小时间阈值
MTUint协商值,超时兜底 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 页面
  • 核心源码:
    • BleManager.java
    • SendBleDataThread.java
    • BleEventCallbackManager.java
    • BleEventCallback.java
    • OnWriteDataCallback.java
Prev
ATT 设备连接与断开管理