macOS下BLE L2CAP SDU未触发Delegate回调问题求助
问题背景
运行在macOS上的C++命令行应用,BLE部分用Objective-C实现,与外设成功建立L2CAP通道后,Wireshark和Apple PacketLogger能捕获到外设发送的SDU,但流事件处理器回调始终未触发。相同BLE代码在其他macOS/iOS项目中运行正常,迁移后设备发现、GATT通信均正常,仅L2CAP消息接收异常。
相关代码
创建CBCentralManager:
dispatch_queue = dispatch_queue_create("de.torrox.ble_event_queue", NULL); manager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_queue options:nil];
L2CAP通道打开回调:
- (void)peripheral:(CBPeripheral *)peripheral didOpenL2CAPChannel:(CBL2CAPChannel *)channel error:(NSError *)error { [channel inputStream].delegate = self; [channel outputStream].delegate = self; [[channel inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [[channel outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [[channel inputStream] open]; [[channel outputStream] open]; ... //A reference to the channel is stored in the outside channel object [channel retain]; ... }
流事件处理函数:
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)event_code { Log( @"stream:handleEvent %@, %lu", stream, event_code ); ... }
调试信息
- 移除同连接事件中的GATT Notification,问题依旧;
- 检测inputStream的
hasBytesAvailable:未加密时返回false,加密后返回预期数据,但回调仍未触发; didOpenL2CAPChannel所在线程为dispatch_queue线程,后续阻塞在libsystem_kernel.dylib__workq_kernreturn + 8;- 主线程阻塞在
boost::asio::io_context::run(); - 检测
currentRunLoop的currentMode为null,传入该值调度流后问题依旧。
问题分析与解决办法
核心原因:RunLoop未持续运行
你把流调度到了GCD工作线程的RunLoop,但GCD的工作线程默认不会自动启动并维持RunLoop。当didOpenL2CAPChannel回调执行完毕后,该线程就会进入休眠或被回收,RunLoop根本没在运行,自然无法检测到流的状态变化,也就触发不了回调。其他项目正常是因为那些项目的RunLoop处于持续运行状态(比如iOS主线程、macOS GUI应用主线程),而当前命令行程序的主线程被boost::asio占用,BLE回调线程没有维持RunLoop。
具体修复步骤
在BLE回调线程启动RunLoop
在didOpenL2CAPChannel回调末尾添加代码,启动当前线程的RunLoop:[[NSRunLoop currentRunLoop] run];注意:
run()会让RunLoop无限运行,若需要停止,可改用runUntilDate:或设置退出标志。改用主线程RunLoop调度流
既然主线程的boost::asio::io_context::run()在运行,可直接把流调度到主线程的RunLoop:[[channel inputStream] scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [[channel outputStream] scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];这样流事件会在主线程触发,主线程的事件循环(boost维护或系统RunLoop)能正常处理回调。
确认代理对象未被释放
命令行程序内存管理需格外注意,确保设置为流代理的self在流的整个生命周期内不会被提前释放,否则代理失效也会导致回调不触发。
关于数据读取的疑问
是的,NSInputStream的新数据会暂存在系统缓冲区,即使不主动调用read:也会存储,但这不是回调不触发的原因——核心问题是RunLoop没运行,无法检测到流的状态变化。就算主动调用read:,也只能读取当前缓冲区的数据,后续新数据还是无法触发回调。
内容的提问来源于stack exchange,提问作者Torsten Robitzki

