我根据需要从 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。
谢谢大家!
最后我发现了我的代码的一些问题。
首先,我需要从运行循环中删除 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
shallow sleep
而不是 deep sleep
之类的
正常的运行循环。 Apple 的框架可能会将事件发送到
主运行循环。这意味着....
OutputStream
和InputStream
不推荐。它们是免费桥接到 CFWriteStream
和 CFReadStream
,并且 Stream API 严重依赖于 CFStream API考虑使用
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) }
}
}