RunLoop 与 DispatchQueue 作为调度器
RunLoop vs DispatchQueue as Scheduler
使用新的 Combine 框架时,您可以指定从发布者接收元素的调度程序。
在这种情况下,RunLoop.main
和 DispatchQueue.main
在将发布者分配给 UI 元素时有很大区别吗?第一个 returns 主线程的 运行 循环和与主线程关联的第二个队列。
我在 Swift 论坛上发布了类似的问题。我鼓励您查看讨论 https://forums.swift.org/t/runloop-main-or-dispatchqueue-main-when-using-combine-scheduler/26635。
我只是复制并粘贴了 Philippe_Hausler
的答案
RunLoop.main as a Scheduler ends up calling RunLoop.main.perform whereas DispatchQueue.main calls DispatchQueue.main.async to do work, for practical purposes they are nearly isomorphic. The only real differential is that the RunLoop call ends up being executed in a different spot in the RunLoop callouts whereas the DispatchQueue variant will perhaps execute immediately if optimizations in libdispatch kick in. In reality you should never really see a difference tween the two.
RunLoop should be when you have a dedicated thread with a RunLoop running, DispatchQueue can be any queue scenario (and for the record please avoid running RunLoops in DispatchQueues, it causes some really gnarly resource usage...). Also it is worth noting that the DispatchQueue used as a scheduler must always be serial to adhere to the contracts of Combine's operators.
我看到 Roy 发布的回复并认为我可以互换使用它们,但实际上我注意到我的应用程序有很大不同。
我在自定义 table 视图单元格中异步加载图像。
只要 table 视图在滚动,使用 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)
RunLoop
的一个重要警告是它是 "not really thread safe"(参见 https://developer.apple.com/documentation/foundation/runloop),因此它可用于延迟块的执行但不能从另一个线程分派它们.如果您正在进行多线程工作(例如异步加载图像),您应该使用 DispatchQueue
返回主线程 UI 线程
使用RunLoop.main
作为Scheduler
和使用DispatchQueue.main
作为Scheduler
其实有很大的区别:
RunLoop.main
运行s 仅当主 运行 循环在 .default
模式下 运行ning 时回调,即不是跟踪触摸和鼠标事件时使用的模式。如果您将 RunLoop.main
用作 Scheduler
,当用户正在触摸或拖动时,您的事件 将不会传送 。
DispatchQueue.main
运行 所有 .common
模式中的回调,其中包括跟踪触摸和鼠标事件时使用的模式。如果您使用 DispatchQueue.main
,您的事件 将在用户进行触摸或拖动时被传递 。
详情
我们可以在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:
方法仅在 default 运行 循环模式下将块调度到 运行。 (这没有记录。)
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:). Under what circumstances are blocks on the main queue executed? In a normal UIKit or AppKit app, the main run loop is responsible for draining the main queue. We can find the run loop implementation in CFRunLoop.c
. The important function is __CFRunLoopRun
, which is much too big to quote in its entirety. Here are 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
模式,然后 CFRunLoopRun
将检查主队列是否准备好排出。否则,它不会检查,因此不会耗尽主队列。
.common
模式包括追踪模式。因此,使用 receive(on: DispatchQueue.main)
的 Combine 管道将 在用户触摸或拖动时传递信号。
Runloop.main在某些情况下可能会失去信号,例如滚动。
大多数时候用DispatchQueue.main~
就可以了
使用新的 Combine 框架时,您可以指定从发布者接收元素的调度程序。
在这种情况下,RunLoop.main
和 DispatchQueue.main
在将发布者分配给 UI 元素时有很大区别吗?第一个 returns 主线程的 运行 循环和与主线程关联的第二个队列。
我在 Swift 论坛上发布了类似的问题。我鼓励您查看讨论 https://forums.swift.org/t/runloop-main-or-dispatchqueue-main-when-using-combine-scheduler/26635。
我只是复制并粘贴了 Philippe_Hausler
的答案RunLoop.main as a Scheduler ends up calling RunLoop.main.perform whereas DispatchQueue.main calls DispatchQueue.main.async to do work, for practical purposes they are nearly isomorphic. The only real differential is that the RunLoop call ends up being executed in a different spot in the RunLoop callouts whereas the DispatchQueue variant will perhaps execute immediately if optimizations in libdispatch kick in. In reality you should never really see a difference tween the two.
RunLoop should be when you have a dedicated thread with a RunLoop running, DispatchQueue can be any queue scenario (and for the record please avoid running RunLoops in DispatchQueues, it causes some really gnarly resource usage...). Also it is worth noting that the DispatchQueue used as a scheduler must always be serial to adhere to the contracts of Combine's operators.
我看到 Roy 发布的回复并认为我可以互换使用它们,但实际上我注意到我的应用程序有很大不同。
我在自定义 table 视图单元格中异步加载图像。
只要 table 视图在滚动,使用 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)
RunLoop
的一个重要警告是它是 "not really thread safe"(参见 https://developer.apple.com/documentation/foundation/runloop),因此它可用于延迟块的执行但不能从另一个线程分派它们.如果您正在进行多线程工作(例如异步加载图像),您应该使用 DispatchQueue
返回主线程 UI 线程
使用RunLoop.main
作为Scheduler
和使用DispatchQueue.main
作为Scheduler
其实有很大的区别:
RunLoop.main
运行s 仅当主 运行 循环在.default
模式下 运行ning 时回调,即不是跟踪触摸和鼠标事件时使用的模式。如果您将RunLoop.main
用作Scheduler
,当用户正在触摸或拖动时,您的事件 将不会传送 。DispatchQueue.main
运行 所有.common
模式中的回调,其中包括跟踪触摸和鼠标事件时使用的模式。如果您使用DispatchQueue.main
,您的事件 将在用户进行触摸或拖动时被传递 。
详情
我们可以在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:
方法仅在 default 运行 循环模式下将块调度到 运行。 (这没有记录。)
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:). Under what circumstances are blocks on the main queue executed? In a normal UIKit or AppKit app, the main run loop is responsible for draining the main queue. We can find the run loop implementation in CFRunLoop.c
. The important function is __CFRunLoopRun
, which is much too big to quote in its entirety. Here are 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
模式,然后 CFRunLoopRun
将检查主队列是否准备好排出。否则,它不会检查,因此不会耗尽主队列。
.common
模式包括追踪模式。因此,使用 receive(on: DispatchQueue.main)
的 Combine 管道将 在用户触摸或拖动时传递信号。
Runloop.main在某些情况下可能会失去信号,例如滚动。 大多数时候用DispatchQueue.main~
就可以了