2. Android BLE开发基础:BluetoothAdapter、BluetoothDevice、BluetoothGatt 的核心用法

说实话,BLE(蓝牙低功耗)开发的门槛不算高,但坑是真不少。我第一次写 BLE 通信时,光扫描就折腾了两天——不是搜索不到设备,就是连上几秒后自动断开。后来发现,核心问题就出在没搞懂底层的三个关键对象:BluetoothAdapter、BluetoothDevice、BluetoothGatt。

今天我就把这哥仨掰开揉碎讲清楚。你如果能把这套基础打牢,后面读写 GATT 特征就跟喝水一样自然。

Android BLE 开发三大核心对象及其关系 BluetoothAdapter 系统蓝牙适配器 getDefaultAdapter() BluetoothDevice 远程蓝牙设备 地址、名称、类型 BluetoothGatt GATT 配置文件 连接、发现服务、读写 startLeScan() connectGatt() connect() 建立连接 discoverServices() 发现服务与特征 read/write 读写特征值 💡 说明: 三个对象是链式调用关系:Adapter 扫描得到 Device,Device 建立 Gatt 连接。 后续所有特征操作都通过 BluetoothGatt 完成,记得在回调里处理状态。

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);
}
⚠️ 我曾经踩过的一个坑:在 Android 12 以上,扫描 BLE 设备需要 BLUETOOTH_SCAN 运行时权限。如果你没动态申请,startLeScan() 会静默失败,连错误 log 都不给。切记!

2.2 BluetoothDevice —— 找到的那个「它」

扫描回调里会返回一个 BluetoothDevice 对象。它代表一个真实的蓝牙外设。说白了,它就是一张名片:上面印着设备地址、名称、绑定状态。

你可以从这个对象里取出很多信息:

  • getAddress() —— MAC 地址,唯一标识
  • getName() —— 广播名称(可能为空)
  • getType() —— 设备类型(经典蓝牙 / BLE / 双模)
  • getBondState() —— 绑定状态

但请注意:这块名片不能直接用来读数据。它只负责告诉你“我在呢”,真正干活还得靠下面这位。

我项目里有个经验:拿到 BluetoothDevice 后,建议先用一个 weak reference 或轻量级的缓存池存起来。原因很简单——当多个扫描周期交叉时,同一个设备可能会被多次回调。如果不做去重,UI 列表会疯狂闪烁。(别问我怎么知道的)

2.3 BluetoothGatt —— 真正的“通信管道”

到了这一步,才算真正开始和蓝牙外设“说话”。BluetoothGatt 是整个 GATT(通用属性配置文件)的客户端代理。所有的服务发现、特征读取、写入、通知订阅,都得通过它。

使用流程大概是:

  1. 连接:调用 device.connectGatt(context, autoConnect, callback)
  2. 发现服务:连接成功后在回调里调 gatt.discoverServices()
  3. 获取特征:通过 gatt.getService(uuid).getCharacteristic(uuid)
  4. 读写:gatt.readCharacteristic() 或 gatt.writeCharacteristic()
  5. 关闭:用完务必 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 特征的应用。