55、蓝牙开发:经典蓝牙搜索配对、BLE低功耗蓝牙、数据收发
蓝牙开发,说简单也简单,说复杂能让你抓狂。我最早接触蓝牙是在一个智能家居项目里,手机连不上设备,日志刷了一屏又一屏,最后发现是UUID写错了。嗯,这种坑,踩过一次就记住了。
今天咱们把蓝牙这块掰开揉碎。经典蓝牙(Classic Bluetooth)和低功耗蓝牙(BLE)是两套不同的协议栈,虽然都叫蓝牙,但用法、场景、代码实现差别很大。你想想看,一个耳机和一颗温湿度传感器,它们需要的蓝牙能力能一样吗?
核心认知:经典蓝牙适合高带宽、持续连接(如音频传输);BLE适合低功耗、小数据量、间歇通信(如传感器、穿戴设备)。Android从4.3开始支持BLE,但经典蓝牙的支持更早。
经典蓝牙:搜索与配对
经典蓝牙的开发流程,说白了就是三板斧:打开蓝牙、搜索设备、配对连接。我习惯先把 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_SCAN、BLUETOOTH_CONNECT 等运行时权限,而且位置权限不再是必须的(如果你不用蓝牙做定位)。但为了兼容旧版本,我通常还是把 ACCESS_FINE_LOCATION 加上,并在代码里做版本判断。
蓝牙开发就是这样,细节很多,但核心流程并不复杂。多动手,多踩坑,慢慢就熟练了。