iOS 18 蓝牙权限弹框不弹?CBCentralManager 一直 Unknown 的排查记录

4 阅读4分钟

一、问题

最近在 App 里接 BLE 设备搜索,遇到一个很怪的问题。

  • 同一套代码,iOS 15 正常弹蓝牙权限,扫描也正常
  • 换到一台 iOS 18 手机,蓝牙权限弹框死活不出现
  • 重装 App 没用
  • 系统设置「隐私与安全性 → 蓝牙」里,也看不到这个 App
  • 代码没有报错,但扫描就是不动

二、排查过程

先看 Info.plist

第一反应当然是看权限文案有没有配:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限以搜索并连接设备</string>

配置是有的。所以不是 Info.plist 漏配。

再看 CBCentralManager 写法

CoreBluetooth 这块也没什么花活。初始化时设置 delegate,等 centralManagerDidUpdateState: 回调,再开始扫描:

self.centralManager = [[CBCentralManager alloc] initWithDelegate:self
                                                           queue:dispatch_get_main_queue()];
​
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    if (central.state == CBManagerStatePoweredOn) {
        [central scanForPeripheralsWithServices:nil options:nil];
    }
}

这部分也没问题。

加日志看状态卡在哪

接着我在原生模块里加了几行日志:

- (void)startBluetoothScan {
    NSLog(@"[Bluetooth] start scan");
    [self setupCentralManagerIfNeeded];
    CBManagerState state = self.centralManager.state;
    NSLog(@"[Bluetooth] state: %ld", (long)state);  // 输出:0 (Unknown)
}
​
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    NSLog(@"[Bluetooth] state updated: %ld", (long)central.state);  // 永远不打印!
}

结果很关键:

[Bluetooth] start scan
[Bluetooth] state: 0     ← Unknown
                          ← centralManagerDidUpdateState 永远不触发

CBCentralManager 是创建成功了,但状态一直停在 Unknown(0)

而且,centralManagerDidUpdateState: 根本不回调。权限弹框也不会出现。

也就是说,问题不在“用户拒绝权限”,而是系统压根没走到权限请求那一步。


三、原因:iOS 18.0 / 18.0.1 的系统 Bug

查了一圈之后,基本可以确定:这是 iOS 18.0 和 18.0.1 的系统 Bug,Apple 后面在 iOS 18.1 修了。

它的表现很像 App 代码有问题,但实际卡在系统权限进程上:

  • CBCentralManager 初始化后,负责权限判定的系统守护进程 tccd(TCC daemon)出现异常卡死
  • 系统不弹蓝牙权限框
  • centralManagerDidUpdateState: 不回调
  • CBManagerState 一直是 Unknown(0)
  • 重装 App 没用,因为卡住的是系统进程,不是 App

受影响版本: iOS 18.0、iOS 18.0.1 修复版本: iOS 18.1+(当前最新 iOS 18.5 均已修复)

相关讨论:Apple Developer Forums Thread #763271


四、解决方案

方案 A:升级系统

最干净的办法,就是升级到 iOS 18.1 或更高版本

如果用户能升级,这个问题就没必要在代码里绕太多。

方案 B:代码里做一次自动兜底

但实际产品里,你不能要求所有用户马上升级系统。

所以我在代码里做了一个兜底:如果 5 秒内 centralManagerDidUpdateState: 还没回来,就认为可能踩到了这个 iOS 18 Bug。然后销毁当前 CBCentralManager,重新创建一次。

有些设备第二次或第三次初始化时,tccd 会恢复,权限弹框也能正常弹出来。

Native 层实现(Objective-C):

// 属性声明
@property (nonatomic, assign) NSInteger bluetoothInitRetryCount;
@property (nonatomic, assign) BOOL waitingForBluetoothState;
​
- (void)startBluetoothScan {
    [self setupCentralManagerIfNeeded];
​
    CBManagerState state = self.centralManager.state;
    if (state == CBManagerStateUnknown) {
        self.waitingForBluetoothState = YES;
        NSLog(@"[Bluetooth] state unknown, waiting... (retry=%ld)", (long)self.bluetoothInitRetryCount);
​
        __weak typeof(self) weakSelf = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)),
                       dispatch_get_main_queue(), ^{
            __strong typeof(weakSelf) strongSelf = weakSelf;
            if (!strongSelf || !strongSelf.waitingForBluetoothState) return;
​
            strongSelf.waitingForBluetoothState = NO;
​
            if (strongSelf.bluetoothInitRetryCount < 2) {
                // 自动重试:销毁旧实例,重新创建
                strongSelf.bluetoothInitRetryCount++;
                NSLog(@"[Bluetooth] iOS 18 bug, auto-retry #%ld", (long)strongSelf.bluetoothInitRetryCount);
                strongSelf.centralManager = nil;
                [strongSelf startBluetoothScan];
            } else {
                // 3 次均失败,通知前端
                NSLog(@"[Bluetooth] all retries exhausted");
                strongSelf.bluetoothInitRetryCount = 0;
                [strongSelf notifyScanFailed:@{
                    @"code": @(-2),
                    @"data": @{@"errorMessage": @"蓝牙初始化失败(iOS 18 已知问题),请重启手机后重试"}
                }];
            }
        });
        return;
    }
​
    self.bluetoothInitRetryCount = 0;
    [self scanForNearbyDevices];
}

前端处理(Vue / UniApp):

bluetoothModule.startScan({ timeout: 8 }, (result) => {
  if (result.code !== 0) {
    this.isSearching = false
    if (result.code === 2) {
      // 用户手动拒绝了蓝牙权限
      uni.showModal({
        title: '蓝牙权限未开启',
        content: '请前往手机"设置-隐私与安全性-蓝牙"中,为本应用开启蓝牙权限',
        confirmText: '去设置',
        success: (res) => { if (res.confirm) uni.openAppAuthorizeSetting() }
      })
    } else if (result.code === -2) {
      // iOS 18 Bug:重试全部失败
      uni.showModal({
        title: '蓝牙初始化失败',
        content: '蓝牙系统异常(iOS 18 已知问题),请重启手机后再试',
        showCancel: false,
        confirmText: '知道了'
      })
    } else {
      uni.showToast({ title: result?.data?.errorMessage || '搜索设备失败', icon: 'none' })
    }
    return
  }
  // 正常处理扫描结果...
})

方案 C:还是不行,就提示重启

如果自动重试也没救,那就提示用户重启手机

重启会让 tccd 重新起来,权限系统一般也就恢复了。

不建议一上来就让用户“还原网络设置”。这个操作太重,而且会影响 Wi-Fi、蜂窝网络、VPN 等配置。对用户来说,成本明显更高。


五、错误码怎么处理

code含义前端处理
0成功 / 扫描中正常处理设备数据
2用户拒绝蓝牙权限引导至系统设置开启
-2iOS 18 Bug 重试耗尽提示重启手机
其他负数蓝牙未开启、不支持等显示 errorMessage