第1章:APP端蓝牙扫描与连接

大家好,我是你们的老朋友。今天咱们聊聊美容仪APP开发中最基础、也最容易踩坑的一环——蓝牙扫描与连接。

说实话,我刚开始做蓝牙开发那会儿,也被iOS和Android的差异折腾得够呛。明明同一个设备,iOS上搜不到,Android上连不上,这种问题我遇到过不下十次。今天就把这些经验掰开揉碎了讲给你听。

1.1 BLE扫描:iOS和Android的差异

先说说扫描。BLE扫描,说白了就是让手机去找周围有哪些蓝牙设备在“喊话”。

iOS平台

  • iOS的扫描由系统统一管理,你只需要调用 CBCentralManagerscanForPeripherals 方法
  • 有个坑:iOS不允许扫描时指定特定的Service UUID以外的过滤条件。我当年想按设备名过滤,结果发现根本不行
  • 扫描回调是异步的,每次发现设备都会调用 didDiscover 代理方法
// iOS 扫描示例
centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    // 处理发现的设备
    print("发现设备: \(peripheral.name ?? "未知")")
}

Android平台

  • Android 5.0+ 使用 BluetoothLeScanner 进行扫描
  • 可以设置更灵活的过滤条件,比如设备名、MAC地址、Service UUID
  • 注意:Android 6.0+ 需要动态申请位置权限,否则扫描不到任何设备。这个我吃过亏,用户反馈说“你们的APP怎么搜不到设备”,结果发现是权限没给
// Android 扫描示例
BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
ScanFilter filter = new ScanFilter.Builder()
    .setServiceUuid(new ParcelUuid(serviceUUID))
    .build();
ScanSettings settings = new ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build();
scanner.startScan(Arrays.asList(filter), settings, scanCallback);

核心差异总结

  • iOS:系统级管理,回调简单,但灵活性差
  • Android:开发者控制力强,但权限和兼容性问题多

1.2 连接与断开:稳定是第一要务

扫描到设备后,下一步就是连接。嗯,这里要注意,连接不是一次就能成功的。

iOS连接流程

  1. 调用 centralManager.connect(peripheral, options: nil)
  2. 等待 didConnect 回调
  3. 连接成功后,需要调用 discoverServices 发现服务
  4. 再调用 discoverCharacteristics 发现特征
// iOS 连接与发现服务
centralManager.connect(peripheral, options: nil)

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("连接成功")
    peripheral.delegate = self
    peripheral.discoverServices([serviceUUID])
}

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    // 发现服务后,再发现特征
    for service in peripheral.services! {
        peripheral.discoverCharacteristics([charUUID], for: service)
    }
}

Android连接流程

  1. 调用 bluetoothDevice.connectGatt(context, false, gattCallback)
  2. onConnectionStateChange 中判断连接状态
  3. 连接成功后,系统会自动调用 onServicesDiscovered
// Android 连接与发现服务
BluetoothGatt gatt = device.connectGatt(context, false, 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) {
        // 服务发现成功,可以操作特征了
    }
});

常见连接失败问题

  • iOS 133错误:设备连接超时。我遇到过,通常是设备端蓝牙模块没准备好
  • Android 133错误:GATT连接失败。多半是设备端异常断开或信号太弱
  • 连接后立即断开:检查是否在连接过程中调用了 disconnectclose

1.3 重连逻辑:让用户体验更丝滑

美容仪这种设备,用户可能用着用着就走远了,或者设备没电了。重连机制必须做好。

我个人习惯的做法是:

  • 维护一个“已配对设备”列表,保存设备的UUID或MAC地址
  • 当连接断开时,自动尝试重连,最多重试3次
  • 每次重连间隔2秒,避免频繁请求
  • 如果3次都失败,提示用户手动连接
// 伪代码:重连逻辑
int retryCount = 0;
while (retryCount < 3 && !isConnected) {
    connectToDevice(device);
    Thread.sleep(2000);
    retryCount++;
}
if (!isConnected) {
    showToast("连接失败,请手动重试");
}

避坑指南

我曾经在重连时忘记释放旧的GATT实例,导致内存泄漏。Android上每个 connectGatt 调用都会创建一个新实例,记得在重连前调用 gatt.close()

1.4 常见连接失败问题汇总

问题现象 可能原因 解决方案
iOS扫描不到设备 未添加蓝牙权限、设备未广播 检查Info.plist中的 NSBluetoothAlwaysUsageDescription
Android扫描不到设备 未获取位置权限、蓝牙未开启 动态申请 ACCESS_FINE_LOCATION 权限
连接后立即断开 设备端连接超时、服务发现失败 检查设备端广播间隔和连接参数
重连失败 设备已离开、GATT实例未释放 重连前调用 close(),增加重试间隔

好了,这一章的内容就到这里。说白了,蓝牙扫描连接就是“找设备、连设备、保连接”这三步。iOS和Android各有各的脾气,但核心逻辑是一样的。下一章咱们聊聊数据收发和指令对接,那才是真正让美容仪动起来的关键。

记住:调试蓝牙时,多看看日志,多试试不同手机。我当年就是靠着一台iPhone 6和一台小米6,踩遍了所有坑。希望你能少走弯路。