2. Android BLE开发基础:BluetoothAdapter、BluetoothDevice、BluetoothGatt 的核心用法
说实话,BLE(蓝牙低功耗)开发的门槛不算高,但坑是真不少。我第一次写 BLE 通信时,光扫描就折腾了两天——不是搜索不到设备,就是连上几秒后自动断开。后来发现,核心问题就出在没搞懂底层的三个关键对象:BluetoothAdapter、BluetoothDevice、BluetoothGatt。
今天我就把这哥仨掰开揉碎讲清楚。你如果能把这套基础打牢,后面读写 GATT 特征就跟喝水一样自然。
2.1 BluetoothAdapter —— 系统的蓝牙开关
一句话:它就是 Android 系统蓝牙的“遥控器”。你要打开蓝牙、扫描设备、获取本地蓝牙信息,都绕不开它。
我个人的习惯是:在每个需要 BLE 的页面里,先检查设备是否支持 BLE,不支持就直接弹提示退出。避免用户干等。
| 方法 | 作用 |
|---|---|
| getDefaultAdapter() | 获取本地蓝牙适配器 |
| isEnabled() | 检查蓝牙是否开启 |
| startLeScan() / stopLeScan() | 低功耗扫描(API 21 前) |
| getBluetoothLeScanner() | 推荐的新版扫描接口(API 21+) |
| enable() / disable() | 启停蓝牙(需权限) |
// 标准初始化代码
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
// 设备不支持蓝牙
Log.e("BLE", "此设备不支持蓝牙");
return;
}
if (!bluetoothAdapter.isEnabled()) {
// 请求用户打开蓝牙
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
BLUETOOTH_SCAN 运行时权限。如果你没动态申请,startLeScan() 会静默失败,连错误 log 都不给。切记!
2.2 BluetoothDevice —— 找到的那个「它」
扫描回调里会返回一个 BluetoothDevice 对象。它代表一个真实的蓝牙外设。说白了,它就是一张名片:上面印着设备地址、名称、绑定状态。
你可以从这个对象里取出很多信息:
getAddress()—— MAC 地址,唯一标识getName()—— 广播名称(可能为空)getType()—— 设备类型(经典蓝牙 / BLE / 双模)getBondState()—— 绑定状态
但请注意:这块名片不能直接用来读数据。它只负责告诉你“我在呢”,真正干活还得靠下面这位。
我项目里有个经验:拿到 BluetoothDevice 后,建议先用一个 weak reference 或轻量级的缓存池存起来。原因很简单——当多个扫描周期交叉时,同一个设备可能会被多次回调。如果不做去重,UI 列表会疯狂闪烁。(别问我怎么知道的)
2.3 BluetoothGatt —— 真正的“通信管道”
到了这一步,才算真正开始和蓝牙外设“说话”。BluetoothGatt 是整个 GATT(通用属性配置文件)的客户端代理。所有的服务发现、特征读取、写入、通知订阅,都得通过它。
使用流程大概是:
- 连接:调用
device.connectGatt(context, autoConnect, callback) - 发现服务:连接成功后在回调里调
gatt.discoverServices() - 获取特征:通过
gatt.getService(uuid).getCharacteristic(uuid) - 读写:
gatt.readCharacteristic()或gatt.writeCharacteristic() - 关闭:用完务必
gatt.close()
// 建立连接(推荐使用自动重连 = false,不然回调会很鬼畜)
bluetoothDevice.connectGatt(context, false, 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) {
// 找到你需要的服务 and 特征
BluetoothGattService service = gatt.getService(UUID.fromString("你的服务UUID"));
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString("你的特征UUID"));
gatt.readCharacteristic(characteristic);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
byte[] data = characteristic.getValue();
// 处理读取到的数据
}
}
});
⚠️ 我曾经炸过的一个问题:GATT 操作不能并发!你不能同时调用 readCharacteristic() 和 writeCharacteristic()。更坑的是,很多国产手机连接成功后会立刻触发 onMtuChanged,如果在那个回调里又去做读写,直接 crash。解决方法很简单:自己维护一个操作队列,等上一个回调结束再发起下一个。
2.4 三个对象的关系总结
你看上面的流程图就明白了:
- BluetoothAdapter 负责“开关”和“扫描”
- BluetoothDevice 负责“标识”和“入口”
- BluetoothGatt 负责“通信”和“数据交互”
说白了,你只要理清这条链:开关 → 找设备 → 连上它 → 读写数据。后面哪怕遇到多连接、重连、重扫,本质都是这三个对象的排列组合。
💡 我自己的小工具:写了一个 BleManager 单例,里面维护一个 ConcurrentHashMap<String, BluetoothGatt>,按 MAC 地址做键。这样能清晰管理多设备连接,而且方便在 Activity 销毁时统一 close() 所有 Gatt。你想想看,要是每个页面自己维护一个 Gatt 引用,一旦 Activity 重建,就等着 memory leak 吧。
2.5 避坑指南(我流过的血)
| 坑点 | 表现 | 解决方案 |
|---|---|---|
| Gatt 回调在子线程 | 更新 UI 直接崩溃 | 回调里用 Handler / runOnUiThread 切回主线程 |
| 未关闭 Gatt | 内存泄漏、蓝牙服务变慢 | 在 onDestroy / onDisconnected 里调用 close() |
| 同时发起多个读写 | 部分操作永远不会回调 | 使用串行队列,一次只做一个操作 |
| 扫描回调被系统抑制 | 扫描不到设备 | 申请 ACCESS_FINE_LOCATION 权限(Android 10 以下)或 BLUETOOTH_SCAN(Android 12+) |
嗯,这章内容就这么多。记住这三个对象的关系和生命周期,你的 BLE 应用至少不会在启动时就挂了。后面我们进入实战,我会带着你一步步写一个能真正读写 GATT 特征的应用。