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

一、问题

最近在 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 用户拒绝蓝牙权限 引导至系统设置开启
-2 iOS 18 Bug 重试耗尽 提示重启手机
其他负数 蓝牙未开启、不支持等 显示 errorMessage
posted @ 2026-05-26 19:11  海冠军  阅读(10)  评论(0)    收藏  举报