CBCentralManager+Block回调+NSThread睡眠组合的异常行为原因咨询
你的问题核心在于阻塞代码占用了CBCentralManager的代理回调队列,导致蓝牙扫描的发现回调完全无法执行。我来一步步拆解这个逻辑:
1. CBCentralManager的代理队列机制
你初始化CBCentralManager时,创建了一个并发队列BTManagerHandler,并指定它作为代理回调的执行队列。根据CoreBluetooth的设计,所有代理方法(包括centralManagerDidUpdateState和centralManager:didDiscoverPeripheral:...)都会在这个队列上调度执行。
2. Block的执行上下文
你的onPoweredOn Block是在centralManagerDidUpdateState方法内部调用的——而这个方法本身就运行在你指定的BTManagerHandler队列上。当你在Block里调用[NSThread sleepForTimeInterval:10.0],本质是让当前正在执行这个Block的线程进入了10秒的休眠状态。
虽然你用的是并发队列,但系统的全局线程池资源是有限的,而且CBCentralManager在向队列提交代理回调时,会优先复用已有线程。当这个线程被sleep死死占住,后续的didDiscoverPeripheral回调任务根本没机会被调度执行——哪怕蓝牙确实扫描到了外设,这些回调也无法被处理。更糟的是,你在sleep结束后立刻调用了stopScan,此时CBCentralManager会终止扫描,那些未被处理的发现回调会直接被丢弃,所以你永远看不到centralManager:didDiscoverPeripheral:...被触发。
3. 为什么移到Block外就正常?
当你把sleep移到Block外部时,阻塞的是discoverDevices方法所在的线程(比如主线程或者其他独立线程),而BTManagerHandler队列是完全独立的。这时候CBCentralManager的代理回调可以不受干扰地在自己的队列上执行,自然能正常收到外设发现的通知。
同样,当你不使用Block时,你大概率是在和代理队列无关的线程里执行sleep,所以不会影响代理回调的调度逻辑。
正确的实现方式
绝对不要在代理回调队列里执行长时间阻塞操作,应该把这类操作放到独立的后台队列中,示例代码如下:
+ (void) discoverDevices { TEST* test = nil; @try { test = [[TEST alloc] init]; SomeBlock onPoweredOn = ^(CBCentralManager* manager) { NSLog(@"%@", @"_onPoweredOn_"); [test startScan]; // 将阻塞操作移到独立的后台队列 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [NSThread sleepForTimeInterval:10.0]; // 回到代理队列执行扫描停止和外设处理(保证线程安全) dispatch_async(manager.queue, ^{ [test stopScan]; NSArray<CBPeripheral*>* discoveredPeripherals = test.peripherals; // 处理扫描到的外设 }); }); }; [test init:onPoweredOn]; } @catch(NSException* e) { // 异常处理 } @finally { // 清理操作 } }
这样既实现了延迟停止扫描的需求,又不会阻塞CBCentralManager的代理回调队列,外设发现的回调就能正常触发了。
内容的提问来源于stack exchange,提问作者TheQuestioner

