一、问题
最近在 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 均已修复)
四、解决方案
方案 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 |