iOS 蓝牙开发:从崩溃到稳定

112 阅读7分钟

本文基于真实项目经验,总结了iOS蓝牙开发中的10大常见问题和解决方案,帮你避开那些让人头疼的坑。

前言

在开发蓝牙手柄SDK的过程中,我踩过无数的坑,从多设备连接崩溃到数据丢失,从权限被拒到后台运行失败。这些问题不仅影响用户体验,还可能导致App Store审核被拒。

本文将分享我在实际项目中遇到的所有问题,并提供经过验证的解决方案。无论你是蓝牙开发新手还是老手,都能从中受益。

问题1:线程安全导致的崩溃 ⚠️ 严重

问题描述

这是最常见也是最严重的问题。CoreBluetooth的回调方法(如didUpdateValueForCharacteristicdidDiscoverServices等)可能不在主线程执行,如果直接在这些回调中更新UI或操作共享资源,会导致:

  • 多设备连接时崩溃(EXC_BAD_ACCESS
  • UI更新异常
  • 数据竞争

错误示例

// ❌ 错误:直接在主线程回调中操作
- (void)peripheral:(CBPeripheral *)peripheral 
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic 
             error:(NSError *)error {
    // 可能不在主线程!
    if (self.batteryCallback) {
        self.batteryCallback(battery); // 崩溃风险
    }
}

正确解决方案

// ✅ 正确:确保在主线程执行
- (void)peripheral:(CBPeripheral *)peripheral 
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic 
             error:(NSError *)error {
    // 检查是否在主线程
    if (![NSThread isMainThread]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self peripheral:peripheral 
didUpdateValueForCharacteristic:characteristic 
                      error:error];
        });
        return;
    }
    
    // 检查错误
    if (error) {
        NSLog(@"❌ 读取特征值失败: %@", error.localizedDescription);
        return;
    }
    
    // 安全地更新UI
    if (self.batteryCallback) {
        self.batteryCallback(battery);
    }
}

关键点

  1. 所有CoreBluetooth回调都要检查线程
  2. 使用dispatch_async(dispatch_get_main_queue())切换到主线程
  3. 先检查错误,再处理数据

问题2:连接管理混乱

问题描述

  • 没有连接超时机制,用户等待时间过长
  • 连接状态管理混乱,多个地方同时尝试连接
  • 断开连接后资源没有清理

解决方案

typedef NS_ENUM(NSInteger, GamepadConnectionState) {
    GamepadConnectionStateDisconnected,
    GamepadConnectionStateConnecting,
    GamepadConnectionStateConnected,
    GamepadConnectionStateDisconnecting
};

@interface GamepadManager : NSObject

@property (nonatomic, assign) GamepadConnectionState connectionState;
@property (nonatomic, strong) NSTimer *connectionTimeoutTimer;
@property (nonatomic, assign) NSTimeInterval connectionTimeout;

- (void)connectWithTimeout:(NSTimeInterval)timeout;
- (void)disconnect;
- (void)handleConnectionTimeout;

@end

实现要点

  1. 使用状态机管理连接状态
  2. 实现连接超时机制
  3. 连接前检查当前状态,避免重复连接
  4. 断开连接时清理所有资源

问题3:数据丢失和乱序

问题描述

  • 使用CBCharacteristicWriteWithoutResponse时数据可能丢失
  • 大数据包传输时可能出现乱序
  • 没有数据校验机制

解决方案

// 重要数据使用WithResponse
- (void)writeImportantData:(NSData *)data 
         toCharacteristic:(CBCharacteristic *)characteristic {
    // 使用WithResponse确保数据送达
    [self.peripheral writeValue:data 
               forCharacteristic:characteristic 
                            type:CBCharacteristicWriteWithResponse];
}

// 大数据包分片传输
- (void)writeLargeData:(NSData *)data 
     toCharacteristic:(CBCharacteristic *)characteristic {
    NSInteger mtu = [self.peripheral maximumWriteValueLengthForType:
                     CBCharacteristicWriteWithoutResponse];
    NSInteger chunkSize = MAX(20, mtu - 3); // 预留3字节开销
    
    for (NSInteger i = 0; i < data.length; i += chunkSize) {
        NSInteger length = MIN(chunkSize, data.length - i);
        NSData *chunk = [data subdataWithRange:NSMakeRange(i, length)];
        
        // 等待上一个写入完成
        [self.peripheral writeValue:chunk 
                  forCharacteristic:characteristic 
                               type:CBCharacteristicWriteWithResponse];
        
        // 简单延迟,实际应该使用回调确认
        [NSThread sleepForTimeInterval:0.01];
    }
}

关键点

  1. 重要数据使用CBCharacteristicWriteWithResponse
  2. 大数据包分片传输
  3. 实现数据包序号和校验

问题4:权限配置缺失

问题描述

缺少必要的权限配置会导致:

  • App Store审核被拒
  • 应用崩溃
  • 后台功能无法使用

解决方案

Info.plist中添加:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>此应用需要蓝牙权限以连接和配置游戏手柄</string>

<key>NSBluetoothPeripheralUsageDescription</key>
<string>此应用需要蓝牙权限以连接和配置游戏手柄</string>

<key>UIBackgroundModes</key>
<array>
    <string>bluetooth-central</string>
</array>

权限请求最佳实践

- (void)requestBluetoothPermission {
    // iOS 13+
    if (@available(iOS 13.0, *)) {
        // 系统会自动请求权限
        // 确保Info.plist中有NSBluetoothAlwaysUsageDescription
    } else {
        // iOS 13之前
        // 确保Info.plist中有NSBluetoothPeripheralUsageDescription
    }
}

问题5:后台运行限制

问题描述

iOS对后台蓝牙操作有严格限制:

  • 必须使用特定的serviceUUID扫描
  • 扫描时间有限制
  • 连接可能被系统断开

解决方案

// 后台扫描必须使用serviceUUID
- (void)scanForPeripheralsInBackground {
    // ❌ 错误:全扫描在后台会被限制
    // [self.centralManager scanForPeripheralsWithServices:nil options:nil];
    
    // ✅ 正确:使用特定的serviceUUID
    NSArray<CBUUID *> *serviceUUIDs = @[
        [CBUUID UUIDWithString:@"0000FFE0-0000-1000-8000-00805F9B34FB"]
    ];
    
    NSDictionary *options = @{
        CBCentralManagerScanOptionAllowDuplicatesKey: @NO
    };
    
    [self.centralManager scanForPeripheralsWithServices:serviceUUIDs 
                                                options:options];
}

// 监听应用生命周期
- (void)applicationDidEnterBackground:(UIApplication *)application {
    // 停止全扫描,只保留特定UUID扫描
    [self.centralManager stopScan];
    [self scanForPeripheralsInBackground];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // 恢复完整扫描
    [self scanForPeripherals];
}

问题6:iOS版本兼容性

问题描述

不同iOS版本的API行为不同:

  • iOS 17+ 连接可能被系统取消
  • iOS 13+ 需要新的权限说明
  • 不同版本的MTU协商行为不同

解决方案

// iOS 17+ 连接前停止扫描
- (void)connectPeripheral:(CBPeripheral *)peripheral {
    if (@available(iOS 17.0, *)) {
        // iOS 17+ 必须先停止扫描
        [self.centralManager stopScan];
        
        // 等待一小段时间确保扫描完全停止
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), 
                      dispatch_get_main_queue(), ^{
            [self.centralManager connectPeripheral:peripheral options:nil];
        });
    } else {
        // iOS 17之前可以直接连接
        [self.centralManager connectPeripheral:peripheral options:nil];
    }
}

// MTU协商
- (void)requestMTU:(NSInteger)mtu {
    if ([self.peripheral respondsToSelector:@selector(maximumWriteValueLengthForType:)]) {
        NSInteger maxLength = [self.peripheral maximumWriteValueLengthForType:
                              CBCharacteristicWriteWithoutResponse];
        NSLog(@"当前MTU: %ld", (long)maxLength);
    }
}

问题7:性能优化

问题描述

  • 全扫描性能差,电池消耗大
  • 频繁读写导致设备响应慢
  • 内存泄漏

解决方案

// 优化扫描性能
- (void)optimizedScan {
    // 使用特定的serviceUUID,而不是nil
    NSArray<CBUUID *> *serviceUUIDs = @[
        [CBUUID UUIDWithString:@"你的ServiceUUID"]
    ];
    
    // 不允许重复,减少回调次数
    NSDictionary *options = @{
        CBCentralManagerScanOptionAllowDuplicatesKey: @NO
    };
    
    [self.centralManager scanForPeripheralsWithServices:serviceUUIDs 
                                                options:options];
}

// 写入队列,限制频率
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *writeQueue;
@property (nonatomic, assign) BOOL isWriting;

- (void)writeDataWithThrottle:(NSData *)data 
            toCharacteristic:(CBCharacteristic *)characteristic {
    [self.writeQueue addObject:@{@"data": data, @"characteristic": characteristic}];
    [self processWriteQueue];
}

- (void)processWriteQueue {
    if (self.isWriting || self.writeQueue.count == 0) {
        return;
    }
    
    self.isWriting = YES;
    NSDictionary *item = self.writeQueue.firstObject;
    [self.writeQueue removeObjectAtIndex:0];
    
    [self.peripheral writeValue:item[@"data"] 
              forCharacteristic:item[@"characteristic"] 
                           type:CBCharacteristicWriteWithoutResponse];
    
    // 限制写入频率:每100ms一次
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), 
                  dispatch_get_main_queue(), ^{
        self.isWriting = NO;
        [self processWriteQueue];
    });
}

问题8:错误处理不完善

问题描述

  • 回调方法没有检查error
  • 没有处理peripheral状态变化
  • 错误信息不明确

解决方案

- (void)peripheral:(CBPeripheral *)peripheral 
didDiscoverServices:(NSError *)error {
    // ✅ 检查错误
    if (error) {
        NSLog(@"❌ 发现服务失败: %@", error.localizedDescription);
        [self handleError:error];
        return;
    }
    
    // ✅ 检查peripheral状态
    if (peripheral.state != CBPeripheralStateConnected) {
        NSLog(@"⚠️ Peripheral未连接,状态: %ld", (long)peripheral.state);
        return;
    }
    
    // ✅ 检查数据有效性
    if (!peripheral.services || peripheral.services.count == 0) {
        NSLog(@"⚠️ 未发现任何服务");
        return;
    }
    
    // 处理服务
    for (CBService *service in peripheral.services) {
        [peripheral discoverCharacteristics:nil forService:service];
    }
}

- (void)handleError:(NSError *)error {
    // 根据错误类型提供不同的处理
    switch (error.code) {
        case CBErrorConnectionTimeout:
            // 连接超时
            break;
        case CBErrorPeripheralDisconnected:
            // 设备断开
            break;
        default:
            break;
    }
}

问题9:用户体验问题

问题描述

  • 连接过程无反馈
  • 设备列表更新不及时
  • 权限请求时机不当

解决方案

// 连接进度回调
typedef void(^ConnectionProgressBlock)(CGFloat progress, NSString *status);

@property (nonatomic, copy) ConnectionProgressBlock connectionProgressBlock;

- (void)connectWithProgress:(ConnectionProgressBlock)progressBlock {
    self.connectionProgressBlock = progressBlock;
    
    // 更新进度
    if (progressBlock) {
        progressBlock(0.2, @"正在扫描设备...");
    }
    
    // 扫描
    [self scanForPeripherals];
    
    // 连接后更新进度
    if (progressBlock) {
        progressBlock(0.5, @"正在连接...");
    }
    
    // 发现服务后更新进度
    if (progressBlock) {
        progressBlock(0.8, @"正在初始化...");
    }
    
    // 完成
    if (progressBlock) {
        progressBlock(1.0, @"连接成功");
    }
}

// 设备列表实时更新
- (void)centralManager:(CBCentralManager *)central 
 didDiscoverPeripheral:(CBPeripheral *)peripheral 
     advertisementData:(NSDictionary<NSString *,id> *)advertisementData 
                  RSSI:(NSNumber *)RSSI {
    // 使用通知或KVO更新UI
    [[NSNotificationCenter defaultCenter] 
     postNotificationName:@"GamepadDidDiscover" 
                   object:peripheral 
                 userInfo:@{@"RSSI": RSSI}];
}

问题10:审核和合规性

问题描述

  • App Store审核被拒
  • 隐私合规问题
  • 后台使用未说明

解决方案

  1. 审核备注中详细说明蓝牙功能
  2. 提供测试账号和设备信息
  3. 在隐私政策中说明蓝牙数据使用
  4. 只收集必要的数据
// 隐私合规示例
- (void)collectDeviceInfo:(CBPeripheral *)peripheral {
    // ✅ 只收集必要信息
    NSDictionary *info = @{
        @"name": peripheral.name ?: @"Unknown",
        @"identifier": peripheral.identifier.UUIDString
    };
    
    // ❌ 不要收集RSSI历史、MAC地址等敏感信息
}

最佳实践总结

1. 线程安全

  • ✅ 所有CoreBluetooth回调都切换到主线程
  • ✅ 使用@synchronized保护共享资源
  • ✅ 避免在回调中直接操作UI

2. 连接管理

  • ✅ 使用状态机管理连接状态
  • ✅ 实现连接超时机制
  • ✅ 连接前检查状态,避免重复连接

3. 错误处理

  • ✅ 所有回调方法都检查error
  • ✅ 检查peripheral状态
  • ✅ 提供明确的错误信息

4. 性能优化

  • ✅ 使用特定的serviceUUID扫描
  • ✅ 实现写入队列,限制频率
  • ✅ 避免内存泄漏(使用weak self)

5. 用户体验

  • ✅ 提供连接进度反馈
  • ✅ 实时更新设备列表
  • ✅ 在合适的时机请求权限

6. 兼容性

  • ✅ 处理不同iOS版本的差异
  • ✅ 测试多种设备型号
  • ✅ 提供降级方案

完整示例代码

我在GitHub上提供了一个完整的示例项目,展示了如何正确实现蓝牙手柄管理:

查看完整示例代码

总结

iOS蓝牙开发确实有很多坑,但只要我们遵循最佳实践,注意线程安全、错误处理和用户体验,就能开发出稳定可靠的蓝牙应用。

希望这篇文章能帮助你在蓝牙开发路上少走弯路。如果你遇到了其他问题,欢迎在评论区分享!


作者简介:iOS开发工程师,专注于蓝牙和IoT设备开发,有丰富的蓝牙SDK开发经验。

相关文章

蓝牙Demo项目地址