在使用新的Combine框架时,您可以指定从发布服务器接收元素的调度程序。
在这种情况下,将发布者分配给UI元素时,RunLoop.main
和DispatchQueue.main
之间是否有很大差异?第一个返回主线程的运行循环,第二个返回与主线程关联的队列。
答案 0 :(得分:20)
使用RunLoop.main
作为Scheduler
和使用DispatchQueue.main
作为Scheduler
之间实际上有很大的区别:
RunLoop.main
仅在主运行循环以.default
模式运行时才运行回调,而不是跟踪触摸和鼠标事件时使用的模式。DispatchQueue.main
在所有.common
模式下运行回调,其中包括跟踪触摸和鼠标事件时使用的模式。我们可以在Schedulers+RunLoop.swift
中看到RunLoop
符合Scheduler
的实现。特别是,它是如何实现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)
的Combine管道不会在用户触摸或拖动时传递信号。
我们可以在Schedulers+DispatchQueue.swift中看到DispatchQueue
符合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
,它太大了,无法完整引用。这里是the lines of interest:
#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
模式,那么{{ 1}}将检查主队列是否已准备就绪。否则,它将不会检查,因此不会耗尽主队列。
CFRunLoopRun
模式包括跟踪模式。因此,使用.common
的Combine管道将在用户触摸或拖动时传递信号。
答案 1 :(得分:2)
我已经在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标注中的另一个位置执行,而如果libdispatch中的优化开始了,DispatchQueue变体也许会立即执行。实际上,您永远不会真正看到两者之间的区别。 / p>
RunLoop应该是当您有一个运行RunLoop的专用线程时,DispatchQueue可以是任何队列方案(为了记录起见,请避免在DispatchQueues中运行RunLoops,这会导致一些非常讨厌的资源使用...)。同样值得注意的是,用作调度程序的DispatchQueue必须始终是串行的,以遵守Combine的运算符的合同。
答案 2 :(得分:2)
RunLoop
的一个重要警告是“它不是真正的线程安全”(请参阅https://developer.apple.com/documentation/foundation/runloop),因此它可用于延迟块的执行,但不能从另一个线程中分派它们。如果您要进行多线程工作(例如异步加载图像),则应使用DispatchQueue
返回主UI线程
答案 3 :(得分:1)
我看到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)
答案 4 :(得分:0)
Runloop.main 在某些情况下可能会丢失信号,例如滚动。 大部分时候用DispatchQueue.main就可以了~