BLE低功耗蓝牙的通信链路和经典蓝牙串口完全不同,它基于GATT协议,通过服务、特征和描述符来组织数据。Android设备作为中央设备,需要完成扫描、连接、服务发现、特征读写四个步骤。整个过程是异步回调驱动的,任何一步都需要等待上一步的回调完成后再执行,否则容易出现操作失败或状态错乱。特别是Android不同版本对蓝牙权限的要求差别很大,如果权限处理不当,可能连设备都扫描不到。

一、权限配置与BLE扫描过滤
在Android 12之前,申请定位权限是扫描BLE设备的前置条件,因为BLE信标可能携带位置信息。从Android 12开始,系统把蓝牙权限拆分为BLUETOOTH_SCAN和BLUETOOTH_CONNECT,targetSdk 31以上的应用可以只申请这两个权限,同时把ACCESS_FINE_LOCATION的申请去掉。不过很多项目为了兼容旧版本,仍然保留定位权限。清单文件中需要声明BLE硬件特性,这样应用市场可以过滤不支持的设备。
在AndroidManifest.xml中,权限声明要覆盖三种场景:Android 5.0到11需要ACCESS_FINE_LOCATION,Android 12及以上需要BLUETOOTH_SCAN和BLUETOOTH_CONNECT,而安装时的普通权限BLUETOOTH和BLUETOOTH_ADMIN在旧版本中仍然有效。以下是一个兼容写法:
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
扫描阶段建议使用BluetoothLeScanner配合ScanFilter,不要直接使用已经废弃的startLeScan。通过设置服务UUID过滤,可以减少无关广播包,也能降低耗电。扫描时要设置合理的扫描时长,比如10秒后自动停止,不要无限扫描。扫描回调运行在主线程还是子线程取决于扫描启动时的上下文,如果在Activity中直接调用,通常回调到主线程,可以直接更新列表;如果使用HandlerThread启动,则需要注意线程切换。
下面这段代码展示了如何初始化扫描器并设置过滤条件,注意ParcelUuid.fromString中的UUID要替换成目标设备的服务UUID:
BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter adapter = manager.getAdapter();
if (adapter == null || !adapter.isEnabled()) {
return;
}
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
List<ScanFilter> filters = new ArrayList<>();
ScanFilter filter = new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"))
.build();
filters.add(filter);
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(filters, settings, scanCallback);
权限申请不能只写在清单里,Android 6.0以上需要在运行时动态请求。很多开发者只申请了定位权限,但在Android 12设备上依然扫描不到设备,就是因为没有处理BLUETOOTH_SCAN。可以在onRequestPermissionsResult中同时检查三个权限,全部授权后再启动扫描。
二、建立GATT连接与服务发现
扫描到目标设备后,通过BluetoothDevice的connectGatt方法发起连接。这个方法有几个参数容易误解:第一个参数是Context,第二个参数autoConnect,第三个参数是BluetoothGattCallback,第四个参数transport通常传TRANSPORT_LE。autoConnect为false表示直接连接,速度较快;为true表示在设备可用时自动连接,但连接时间不可控,适合需要后台保持连接的场景。很多BLE设备在电量低或距离远时,直接连接容易超时,可以先用false快速连接,失败后再考虑true。
连接状态变化通过onConnectionStateChange回调。注意这个回调默认运行在Binder线程,不是主线程,因此在回调里不能直接操作UI,需要通过runOnUiThread或Handler切换。回调的status参数也很关键,如果status不是GATT_SUCCESS,说明连接过程中出现错误,比如133表示连接失败或被设备拒绝,8表示设备超时。连接成功后回调的newState是STATE_CONNECTED,这时应该立即调用discoverServices开始服务发现,但要注意不要在回调里直接并发发起多个GATT操作。
服务发现完成后会触发onServicesDiscovered回调,此时可以通过getService拿到目标服务,再通过getCharacteristic拿到目标特征。服务和特征的UUID由硬件厂商定义,常见的有0000ffe0-0000-1000-8000-00805f9b34fb表示串口透传服务,0000ffe1-0000-1000-8000-00805f9b34fb表示写特征,0000ffe1也可能是通知特征。这些UUID如果不匹配,后续读写都会返回false。
BluetoothDevice device = adapter.getRemoteDevice(macAddress);
BluetoothGatt gatt = device.connectGatt(this, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt.close();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattService service = gatt.getService(SERVICE_UUID);
if (service != null) {
BluetoothGattCharacteristic txChar = service.getCharacteristic(TX_UUID);
BluetoothGattCharacteristic rxChar = service.getCharacteristic(RX_UUID);
}
}
}
};
连接建立后,建议主动协商MTU。BLE默认MTU是23字节,去除ATT头部后有效载荷只有20字节。如果设备支持更大的MTU,可以通过requestMtu(247)申请,系统会回调onMtuChanged。MTU越大,单次传输的数据量越大,但需要两端都支持。对于串口透传类设备,很多模组支持MTU 185甚至247,协商成功后可以显著降低分包次数。
另外,connectGatt返回的BluetoothGatt对象是串行操作的基础,同一个对象不要同时调用多个writeCharacteristic或readCharacteristic。因为Android底层GATT客户端实现是单队列,并发调用会导致部分操作返回false或回调丢失。正确的做法是维护一个待发送队列,等当前操作回调后再执行下一个操作。
三、特征读写、通知使能与稳定性处理
数据发送通过写特征完成。写之前需要调用setWriteType设置写入类型,WRITE_TYPE_DEFAULT表示带响应写入,设备会返回确认,可靠但速度慢;WRITE_TYPE_NO_RESPONSE表示无响应写入,速度快,适合连续数据传输。如果发送频繁且每条数据较小,建议使用无响应写入,并在应用层自定义协议保证数据完整性。写入时调用setValue传入字节数组,再调用writeCharacteristic。返回true只代表请求被接受,不代表数据已经到达设备,真正结果要看onCharacteristicWrite回调。
BluetoothGattCharacteristic txChar = service.getCharacteristic(TX_UUID);
txChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
txChar.setValue(dataBytes);
boolean success = gatt.writeCharacteristic(txChar);
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 可以发送下一条数据
}
}
接收数据通常使用通知模式,而不是轮询读取。通知的使能分两步:先调用setCharacteristicNotification开启本地通知,然后向特征对应的CCCD描述符写入ENABLE_NOTIFICATION_VALUE。很多开发者在第一步后忘记写描述符,导致设备端以为通知未开启,从而收不到数据。CCCD描述符的UUID是00002902-0000-1000-8000-00805f9b34fb,写入成功后设备才会主动推送通知。
通知回调onCharacteristicChanged同样运行在Binder线程,不能直接更新UI。收到的数据可能被分包,比如设备发送64字节数据,由于MTU限制会拆成多个20字节的包到达,应用层需要按协议拼接。如果设备使用标准串口透传,通常数据之间没有分隔符,需要根据长度字段或超时机制处理粘包和拆包。
BluetoothGattCharacteristic rxChar = service.getCharacteristic(RX_UUID);
gatt.setCharacteristicNotification(rxChar, true);
BluetoothGattDescriptor cccd = rxChar.getDescriptor(CCCD_UUID);
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(cccd);
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] data = characteristic.getValue();
// 处理收到的数据
}
BLE连接稳定性是实际项目中最棘手的问题。连接断开可能由设备进入休眠、信号干扰、系统蓝牙栈异常等原因引起。onConnectionStateChange中的status非常重要,133通常表示底层连接失败或参数错误,8表示连接超时。可以在断开后延迟几百毫秒再重连,并使用指数退避策略,避免设备还未完全释放资源时频繁重连导致连续失败。重连时建议复用之前的BluetoothDevice对象,并调用connectGatt重新建立连接。
另外,退出页面或停止通信时必须调用gatt.disconnect和gatt.close释放资源,否则连接会一直占用,导致设备无法被其他应用连接。可以先disconnect触发断开回调,再在STATE_DISCONNECTED中调用close。如果直接close而不调用disconnect,有些Android版本可能不会立即断开底层连接。
对于需要持续读写的场景,可以把GATT操作封装到一个单线程队列中,所有读写请求按顺序执行。队列中每个任务只在上一个任务的回调到达后出队,避免并发操作导致133错误。同时可以加超时保护,例如写操作超过2秒未回调,则主动断开重连。实际测试中,这种串行化改造能把断连概率降低一半以上。
Android BLE低功耗蓝牙GATT修改时间:2026-09-22 12:48:43