如何退出`RunLoop`

问题描述 投票:0回答:2

我根据需要从 iOS Foundation SDK 继承

InputStream
。我需要实现工作线程可以休眠直到数据出现在流中的功能。我用来覆盖功能的测试如下:

    func testStreamWithRunLoop() {
        let inputStream = BLEInputStream() // custom input stream subclass
        inputStream.delegate = self
        let len = Int.random(in: 0..<100)
        let randomData = randData(length: len) // random data generation
        
        let tenSeconds = Double(10)
        let oneSecond = TimeInterval(1)
        runOnBackgroundQueueAfter(oneSecond) {
            inputStream.accept(randomData) // input stream receives the data
        }
        let dateInFuture = Date(timeIntervalSinceNow: tenSeconds) // time in 10 sec
        inputStream.schedule(in: .current, forMode: RunLoop.Mode.default) // 
        RunLoop.current.run(until: dateInFuture) // wait for data appear in input stream
        XCTAssertTrue(dateInFuture.timeIntervalSinceNow > 0, "Timeout. RunLoop didn't exit in 1 sec. ")
    }

这里是

InputStream

的覆盖方法
    public override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
        self.runLoop = aRunLoop // save RunLoop object
        var context = CFRunLoopSourceContext() // make context
        self.runLoopSource = CFRunLoopSourceCreate(nil, 0, &context) // make source
        let cfloopMode: CFRunLoopMode = CFRunLoopMode(mode as CFString) 
        CFRunLoopAddSource(aRunLoop.getCFRunLoop(), self.runLoopSource!, cfloopMode)
    }

public func accept(_ data: Data) { 
        guard data.count > 0 else { return }
        
        self.data += data
        delegate?.stream?(self, handle: .hasBytesAvailable)
        
        if let runLoopSource {
            CFRunLoopSourceSignal(runLoopSource)
        }
        if let runLoop {
            CFRunLoopWakeUp(runLoop.getCFRunLoop())
        }
    }

但是调用

CFRunLoopSourceSignal(runLoopSource)
CFRunLoopWakeUp(runLoop.getCFRunLoop())
不会退出 runLoop。

有人知道我哪里弄错了吗?

谢谢大家!

PS:这里是 GitHub 上的 Xcode 项目

swift multithreading foundation nsrunloop cfrunloop
2个回答
1
投票

最后我发现了我的代码的一些问题。

首先,我需要从运行循环中删除 CFRunLoopSource 对象

CFRunLoopRemoveSource()
。根据文档,如果 RunLoop 没有输入源,那么它会立即退出。

    public func accept(_ data: Data) {
        guard data.count > 0 else { return }
        
        self.data += data
        delegate?.stream?(self, handle: .hasBytesAvailable)
        
        if let runLoopSource, let runLoop, let runLoopMode {
            CFRunLoopRemoveSource(runLoop.getCFRunLoop(), runLoopSource, runLoopMode)
        }
        if let runLoop {
            CFRunLoopWakeUp(runLoop.getCFRunLoop())
        }
    }

第二个问题与我使用XCTest环境有关,它的RunLoop由于某些原因没有退出(向社区寻求帮助)。

我使用了真实的应用程序环境并创建了 Thread 子类来检查我的实现。默认情况下,线程有运行循环,没有任何输入源附加到它。我向其中添加了输入流。并使用主线程模拟流接收数据。

这里是运行和休眠的自定义线程实现,直到它从 BLEInputStream 接收到信号

class StreamThread: Thread, StreamDelegate {
    let stream: BLEInputStream
    
    init(stream: BLEInputStream) {
        self.stream = stream
    }
    
    override func main() {
        stream.delegate = self
        stream.schedule(in: .current, forMode: RunLoop.Mode.default)
        print("start()")
        let tenSeconds = Double(10)
        let dateInFuture = Date(timeIntervalSinceNow: tenSeconds)
        RunLoop.current.run(until: dateInFuture)
        print("after 10 seconds")
    }
    
    override func start() {
        super.start()
    }
    
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        if eventCode == .errorOccurred {
            print("eventCode == .errorOccurred")
        }
        else if eventCode == .hasBytesAvailable {
            print("eventCode == .hasBytesAvailable")
        }
    }
}

这里是一些从主线程运行的 UIViewController 方法

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let baseDate = Date.now
        let thread = StreamThread(stream: stream, baseDate: baseDate)
        thread.start()
        print("main thread pauses at \(Date.now.timeIntervalSince(baseDate))")
        Thread.sleep(forTimeInterval: 2) 
        print("stream accepts Data \(Date.now.timeIntervalSince(baseDate))")
        stream.accept(Data([1,2,3]))
    }

这里是结果:

一切都按预期工作——线程休眠直到输入流接收到数据。没有处理器资源消耗。

虽然允许子类化InputStream,但是文档中没有很好的解释如何正确实现自定义InputStream


0
投票
  1. 在Apple的runtime中,NSMainThread和MainRunLoop是非常特殊的对象。 MainRunLoop 做
    shallow sleep
    而不是
    deep sleep
    之类的 正常的运行循环。 Apple 的框架可能会将事件发送到 主运行循环。

这意味着....

  • MainRunLoop 通过 CFRunLoopPerform 和 CFRunLoopSource0 信号自动唤醒
  • 操作系统和框架可能会附加外部资源和观察者
  1. 子类
    OutputStream
    InputStream
    不推荐。它们是免费桥接到
    CFWriteStream
    CFReadStream
    ,并且 Stream API 严重依赖于 CFStream API

CFReadStream, CFWriteStream

考虑使用

CFReadStreamSetDispatchQueue(_:_:)
CFWriteStreamSetDispatchQueue(_:_:)
而不是
RunLoop
这更容易。

extension InputStream {
    
    final var targetQueue:DispatchQueue? {
        get { CFReadStreamCopyDispatchQueue(self) }
        set { CFReadStreamSetDispatchQueue(self, newValue) }
    }
    
}

extension OutputStream {
    
    final var targetQueue:DispatchQueue? {
        get { CFWriteStreamCopyDispatchQueue(self) }
        set { CFWriteStreamSetDispatchQueue(self, newValue) }
    }
    
}
© www.soinside.com 2019 - 2024. All rights reserved.