使用新的组合框架时,您可以指定从发布者接收元素的调度程序。
在将发布者分配给 UI 元素时,RunLoop.main
和在这种情况下是否有很大区别?DispatchQueue.main
第一个返回主线程的运行循环和与主线程关联的第二个队列。
使用新的组合框架时,您可以指定从发布者接收元素的调度程序。
在将发布者分配给 UI 元素时,RunLoop.main
和在这种情况下是否有很大区别?DispatchQueue.main
第一个返回主线程的运行循环和与主线程关联的第二个队列。
RunLoop.main
使用as aScheduler
和 using DispatchQueue.main
as a之间实际上有很大区别Scheduler
:
RunLoop.main
仅当主运行循环在该.default
模式下运行时才运行回调,这不是跟踪触摸和鼠标事件时使用的模式。如果您使用RunLoop.main
as a ,当用户处于触摸或拖动过程中时Scheduler
,您的事件将不会被传递。
DispatchQueue.main
在所有模式下运行回调.common
,包括跟踪触摸和鼠标事件时使用的模式。如果您使用DispatchQueue.main
,您的事件将在用户处于触摸或拖动过程中时传递。
我们可以看到RunLoop
's 的实现符合Scheduler
in Schedulers+RunLoop.swift
。特别是,它的实现方式schedule(options:_:)
如下:
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { self.perform(action) }
这里使用RunLoop
perform(_:)
方法,也就是 Objective-C 方法-[NSRunLoop performBlock:]
。该performBlock:
方法将块调度为仅在默认运行循环模式下运行。(这没有记录。)
UIKit 和 AppKit 在空闲时以默认模式运行运行循环。但是,特别是在跟踪用户交互(如触摸或鼠标按钮按下)时,它们会以不同的非默认模式运行运行循环。因此,当用户触摸或拖动时,使用的组合管道receive(on: RunLoop.main)
不会传递信号。
我们可以在Schedulers+DispatchQueue.swift中看到DispatchQueue
's 一致性的实现。以下是它的实现方式:Scheduler
schedule(options:_:)
public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { let qos = options?.qos ?? .unspecified let flags = options?.flags ?? [] if let group = options?.group { // Distinguish on the group because it appears to not be a call-through like the others. This may need to be adjusted. self.async(group: group, qos: qos, flags: flags, execute: action) } else { self.async(qos: qos, flags: flags, execute: action) } }
因此,使用标准 GCD 方法async(group:qos:flags:execute:)将块添加到队列中。什么情况下会执行主队列上的块?在普通的 UIKit 或 AppKit 应用程序中,主运行循环负责排空主队列。我们可以在CFRunLoop.c
. 重要的功能是__CFRunLoopRun
,它太大了,无法完整引用。以下是感兴趣的行:
#if __HAS_DISPATCH__ __CFPort dispatchPort = CFPORT_NULL; Boolean libdispatchQSafe = pthread_main_np() && ( (HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)) ); if ( libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name) ) dispatchPort = _dispatch_get_main_queue_port_4CF(); #endif
(为了便于阅读,我已经包装了原始源代码行。)这是该代码的作用:如果可以安全地排空主队列,并且它是主运行循环,并且是一种.common
模式,那么CFRunLoopRun
将检查主队列是否准备好排空. 否则,它不会检查,因此不会耗尽主队列。
.common
模式包括跟踪模式。因此,使用的组合管道receive(on: DispatchQueue.main)
将在用户触摸或拖动时传递信号。
我看到 Roy 发布的回复,并认为我可以互换使用它们,但实际上我注意到我的应用程序有很大的不同。
我在自定义表格视图单元格中异步加载图像。RunLoop.main
只要表格视图正在滚动,使用就会阻止加载图像。
subscriber = NetworkController.fetchImage(url: searchResult.artworkURL)
.receive(on: RunLoop.main)
.replaceError(with: #imageLiteral(resourceName: "PlaceholderArtwork"))
.assign(to: \.image, on: artworkImageView)
但是切换到DispatchQueue.main
允许图像在滚动时加载。
subscriber = NetworkController.fetchImage(url: searchResult.artworkURL)
.receive(on: DispatchQueue.main)
.replaceError(with: #imageLiteral(resourceName: "PlaceholderArtwork"))
.assign(to: \.image, on: artworkImageView)
我已经在 Swift 论坛上发布了类似的问题。我鼓励您查看讨论https://forums.swift.org/t/runloop-main-or-dispatchqueue-main-when-using-combine-scheduler/26635。
我只是复制并粘贴Philippe_Hausler的答案
RunLoop.main 作为调度程序最终调用 RunLoop.main.perform 而 DispatchQueue.main 调用 DispatchQueue.main.async 来完成工作,实际上它们几乎是同构的。唯一真正的区别是 RunLoop 调用最终在 RunLoop 标注中的不同位置执行,而 DispatchQueue 变体可能会在 libdispatch 中的优化启动时立即执行。实际上,您永远不会真正看到两者之间的差异。
RunLoop 应该是当你有一个运行 RunLoop 的专用线程时,DispatchQueue 可以是任何队列场景(为了记录,请避免在 DispatchQueues 中运行 RunLoops,它会导致一些非常粗糙的资源使用......)。另外值得注意的是,用作调度程序的 DispatchQueue 必须始终是串行的,以遵守 Combine 运营商的合同。
一个重要的警告RunLoop
是它“不是真正的线程安全”(参见https://developer.apple.com/documentation/foundation/runloop),所以它可以用来延迟块的执行,但不能从另一个分派它们线。如果您正在执行多线程工作(例如异步加载图像),您应该使用 aDispatchQueue
返回主 UI 线程
Runloop.main 在某些情况下可能会丢失他的信号,例如滚动。大多数时候,使用DispatchQueue.main就可以了~