55、蓝牙开发:经典蓝牙搜索配对、BLE低功耗蓝牙、数据收发

蓝牙开发,说简单也简单,说复杂能让你抓狂。我最早接触蓝牙是在一个智能家居项目里,手机连不上设备,日志刷了一屏又一屏,最后发现是UUID写错了。嗯,这种坑,踩过一次就记住了。

今天咱们把蓝牙这块掰开揉碎。经典蓝牙(Classic Bluetooth)和低功耗蓝牙(BLE)是两套不同的协议栈,虽然都叫蓝牙,但用法、场景、代码实现差别很大。你想想看,一个耳机和一颗温湿度传感器,它们需要的蓝牙能力能一样吗?

核心认知:经典蓝牙适合高带宽、持续连接(如音频传输);BLE适合低功耗、小数据量、间歇通信(如传感器、穿戴设备)。Android从4.3开始支持BLE,但经典蓝牙的支持更早。

Android 蓝牙开发知识体系 经典蓝牙 (Classic) BluetoothAdapter / BluetoothDevice 搜索 → 配对 → 建立RFCOMM Socket 数据收发:InputStream / OutputStream 低功耗蓝牙 (BLE) BluetoothLeScanner / BluetoothGatt 扫描 → 连接 → 发现服务/特征值 数据收发:read / write / notify 不同协议栈 权限管理 · 动态申请 · 后台扫描限制 · 多设备兼容

经典蓝牙:搜索与配对

经典蓝牙的开发流程,说白了就是三板斧:打开蓝牙、搜索设备、配对连接。我习惯先把 BluetoothAdapter 拿到手,这是所有操作的入口。

// 获取蓝牙适配器
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
    // 设备不支持蓝牙
    return;
}
// 如果没打开,请求打开
if (!bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

搜索设备用 startDiscovery(),它会扫描周围所有可见的经典蓝牙设备。我记得有一次在办公室里测试,搜出来二十多个设备,耳机、键盘、手机……广播接收器里处理 BluetoothDevice.ACTION_FOUND 就行。

注意:搜索过程很耗电,而且会干扰已建立的连接。我个人建议搜索完成后立即调用 cancelDiscovery()。另外,从Android 6.0开始需要 ACCESS_FINE_LOCATION 权限,因为蓝牙扫描可能被用于定位。

配对其实不需要写太多代码。当你尝试连接一个未配对的设备时,系统会自动弹出配对对话框。当然你也可以用 createBond() 主动发起配对。我曾经踩过一个坑:配对成功后直接去连接Socket,结果连不上。后来发现配对和连接之间需要一点延迟,等系统把配对状态更新完再连。

经典蓝牙:数据收发

连接用 BluetoothSocket,这是基于RFCOMM协议的套接字。服务端和客户端都要指定相同的UUID,这个UUID相当于一个“服务标识”。

// 客户端连接示例
UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(macAddress);
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(MY_UUID);
socket.connect();

// 获取输入输出流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();

// 发送数据
String msg = "Hello from Android";
outputStream.write(msg.getBytes());

// 接收数据(需要在子线程中)
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);

这里有个关键点:connect()read() 都是阻塞操作,必须在子线程里跑。我见过不少新手直接在UI线程里调 connect(),结果界面卡死,ANR弹出来一脸懵。嗯,用 AsyncTask 或者 Thread 都行,现在推荐用协程或者 HandlerThread

个人经验:经典蓝牙的数据收发没有“分包”机制,如果你要传大文件,需要自己定义协议头,比如前4个字节表示数据长度。我曾经做一个文件传输工具,没做分包,结果超过2KB的数据就丢包了,后来老老实实加了长度头。

BLE低功耗蓝牙:扫描与连接

BLE和经典蓝牙最大的区别在于:BLE用GATT(通用属性协议)来通信。你不再直接读写流,而是操作“特征值”(Characteristic)。

先扫描。Android 5.0之后推荐用 BluetoothLeScanner,比老的 startLeScan() 更灵活。

BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
ScanSettings settings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式
        .build();
List<ScanFilter> filters = new ArrayList<>();
// 可以按设备名或UUID过滤
filters.add(new ScanFilter.Builder().setDeviceName("MySensor").build());

scanner.startScan(filters, settings, scanCallback);

// 回调
private ScanCallback scanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        BluetoothDevice device = result.getDevice();
        int rssi = result.getRssi(); // 信号强度
        // 处理设备
    }
};

扫描到设备后,用 connectGatt() 连接。注意,BLE连接也是异步的,通过 BluetoothGattCallback 接收结果。

BluetoothGatt gatt = device.connectGatt(context, false, gattCallback);

private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // 连接成功,开始发现服务
            gatt.discoverServices();
        }
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        // 拿到服务,然后找到你要的特征值
        BluetoothGattService service = gatt.getService(UUID_SERVICE);
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR);
        // 后面就可以读写或订阅通知了
    }
};

避坑指南:我曾经在 onConnectionStateChange 里直接调 discoverServices(),但有时候状态码返回133(GATT_ERROR),重连一次就好了。后来我加了重试机制,最多重试3次,每次间隔500ms。另外,connectGatt 的第二个参数 autoConnect 设为 false 更可控,设为 true 的话,设备断开后会自动重连,有时候反而麻烦。

BLE数据收发:读写与通知

BLE的数据收发有三种方式:Read、Write、Notify。Read是客户端主动读,Write是客户端主动写,Notify是设备主动推数据给客户端。

我最常用的是Notify,因为传感器数据需要实时更新。订阅通知需要先开启特征值的通知开关,然后调用 setCharacteristicNotification,同时还要给对应的CCC描述符(0x2902)写值。

// 开启通知
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);

// 接收通知
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    byte[] data = characteristic.getValue();
    // 处理接收到的数据
}

// 写数据
characteristic.setValue(bytes);
gatt.writeCharacteristic(characteristic);

写数据时要注意MTU(最大传输单元)。默认MTU是23字节,其中3字节是协议头,实际有效载荷只有20字节。如果要发大包,需要先协商MTU:gatt.requestMtu(512)。我在一个项目中需要发送图片缩略图,没协商MTU,数据被截断了,调试了半天才发现是MTU的问题。

特性 经典蓝牙 BLE
功耗 高(持续连接) 极低(休眠+唤醒)
传输速率 ~2 Mbps ~1 Mbps(实际更低)
数据模型 Socket流 GATT特征值
典型应用 音频、文件传输 传感器、穿戴设备
Android API BluetoothSocket BluetoothGatt

我的习惯:开发BLE应用时,我会先用nRF Connect这类工具扫描设备,看看它有哪些服务和特征值,UUID是什么。这样写代码时心里有底,不用盲猜。另外,调试时多打印 onConnectionStateChange 的status值,很多问题看状态码就能定位。

最后提一下权限。Android 12开始,蓝牙扫描需要 BLUETOOTH_SCANBLUETOOTH_CONNECT 等运行时权限,而且位置权限不再是必须的(如果你不用蓝牙做定位)。但为了兼容旧版本,我通常还是把 ACCESS_FINE_LOCATION 加上,并在代码里做版本判断。

蓝牙开发就是这样,细节很多,但核心流程并不复杂。多动手,多踩坑,慢慢就熟练了。


公众号:蓝海资料掘金营,微信deep3321