iOS自定义InputStream子类中RunLoop无法提前退出的问题排查
问题分析与解决方案
你的测试失败是因为RunLoop在处理完数据事件后,仍有活跃的输入源(你添加的CFRunLoopSource)存在,因此不会提前退出,直到等待到10秒的截止时间。以下是具体问题点和修复方案:
核心问题
RunLoop源生命周期未正确管理:你在
schedule(in:forMode:)中添加了一个CFRunLoopSource到RunLoop,但从未移除它。RunLoop.run(until:)仅会在以下场景提前退出:- 到达指定截止日期;
- RunLoop中无任何待处理的输入源、定时器或观察者。
由于你添加的source始终存在,即使数据触发了delegate回调,RunLoop仍会持续运行至10秒截止时间,导致断言失败。
空的CFRunLoopSourceContext:你创建的CFRunLoopSource使用了空上下文,未定义source被signal时的处理逻辑,导致RunLoop被唤醒后无法正确完成该source的事件处理流程。
修复方案
步骤1:完善CFRunLoopSource的回调与生命周期
修改schedule(in:forMode:)方法,添加source的处理回调,并在事件完成后移除source:
public override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { self.runLoop = aRunLoop var context = CFRunLoopSourceContext() // 设置source触发时的处理逻辑 context.perform = { info in guard let stream = info?.assumingMemoryBound(to: BLEInputStream.self).pointee else { return } // 处理完成后从RunLoop移除source,让RunLoop可以退出 if let runLoopSource = stream.runLoopSource, let runLoop = stream.runLoop { let cfMode = CFRunLoopMode(mode as CFString) CFRunLoopRemoveSource(runLoop.getCFRunLoop(), runLoopSource, cfMode) stream.runLoopSource = nil } } // 将当前流对象作为上下文参数传入 context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) self.runLoopSource = CFRunLoopSourceCreate(nil, 0, &context) let cfloopMode = CFRunLoopMode(mode as CFString) CFRunLoopAddSource(aRunLoop.getCFRunLoop(), self.runLoopSource!, cfloopMode) }
步骤2:简化accept(_:)方法逻辑
CFRunLoopSourceSignal会自动唤醒RunLoop,无需额外调用CFRunLoopWakeUp,调整后的代码:
public func accept(_ data: Data) { guard data.count > 0 else { return } self.data += data delegate?.stream?(self, handle: .hasBytesAvailable) if let runLoopSource { CFRunLoopSourceSignal(runLoopSource) } }
步骤3:补充unschedule方法避免内存泄漏
在unschedule(from:forMode:)中也移除source,确保资源正确释放:
public override func unschedule(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) { if let runLoopSource { let cfMode = CFRunLoopMode(mode as CFString) CFRunLoopRemoveSource(aRunLoop.getCFRunLoop(), runLoopSource, cfMode) self.runLoopSource = nil self.runLoop = nil } }
修改后,当accept调用并触发source信号时,RunLoop会执行回调移除source,随后因无活跃输入源而提前退出,此时dateInFuture.timeIntervalSinceNow仍大于0,断言会通过。
内容的提问来源于stack exchange,提问作者Ihar Katkavets
相关产品推荐
相关产品推荐

