RunLoop 与 DispatchQueue 作为调度器

RunLoop vs DispatchQueue as Scheduler

使用新的 Combine 框架时,您可以指定从发布者接收元素的调度程序。

在这种情况下,RunLoop.mainDispatchQueue.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中看到RunLoopScheduler的一致性的实现。特别是,这里是它如何实现 schedule(options:_:):

    public func schedule(options: SchedulerOptions?,
                         _ action: @escaping () -> Void) {
        self.perform(action)
    }

这里使用了RunLoopperform(_:)的方法,也就是Objective-C的方法-[NSRunLoop performBlock:]performBlock: 方法仅在 default 运行 循环模式下将块调度到 运行。 (这没有记录。)

UIKit 和 AppKit 运行 运行 空闲时在默认模式下循环。但是,特别是在跟踪用户交互(如触摸或鼠标按钮按下)时,它们 运行 运行 以不同的非默认模式循环。因此,使用 receive(on: RunLoop.main) 的 Combine 管道将不会 在用户触摸或拖动时传递信号。

我们可以在Schedulers+DispatchQueue.swift中看到DispatchQueueScheduler的一致性的实现。下面是它如何实现 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~

就可以了