You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS自定义InputStream子类中RunLoop无法提前退出的问题排查

问题分析与解决方案

你的测试失败是因为RunLoop在处理完数据事件后,仍有活跃的输入源(你添加的CFRunLoopSource)存在,因此不会提前退出,直到等待到10秒的截止时间。以下是具体问题点和修复方案:

核心问题

  1. RunLoop源生命周期未正确管理:你在schedule(in:forMode:)中添加了一个CFRunLoopSource到RunLoop,但从未移除它。RunLoop.run(until:)仅会在以下场景提前退出:

    • 到达指定截止日期;
    • RunLoop中无任何待处理的输入源、定时器或观察者。
      由于你添加的source始终存在,即使数据触发了delegate回调,RunLoop仍会持续运行至10秒截止时间,导致断言失败。
  2. 空的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 21:41:11