iOS / Swift(UI)始终运行的后台计算线程的高效实现-GCD案例?
Posted
技术标签:
【中文标题】iOS / Swift(UI)始终运行的后台计算线程的高效实现-GCD案例?【英文标题】:iOS/Swift(UI) Efficient implementation of Background calculation thread always running - GCD case? 【发布时间】:2021-12-28 11:26:48 【问题描述】:为了简化我的问题:我有一个 巨大的计算函数,有 3 个输入 和 2 个输出。我的输入变量是 SwiftUI ObservableObject
中的 @Published
SwiftUI 变量。如果用户采取行动,UI 会经常修改这些输入变量。输出变量也是同一类中的@Published
变量。如前所述,背景计算量很大 - 因此,当三个输入之一发生变化时,我需要一直运行计算。如果在计算期间输入发生变化,则需要使用当前(新)值进行新计算。这应该尽可能高效和高效。
我来自 android 编程,在 Java 中我创建了一个非常有效的解决方案,它拥有一个始终运行的后台线程,在不需要新计算时休眠,并在其中一个输入时唤醒(用 Java 语言通知)改变了。然后它通过请求最新的输入参数来计算新的结果。如果这些输入参数在计算过程中发生了变化,线程会以最新的进行另一次计算,以此类推……
但是,我不确定如何在 Swift 中实现这一点。我看到我们可以在一个额外的队列上向 GCD 提交任务以进行后台处理。因此,不可能让后台线程运行、暂停并等待新输入并计算它们——对吗?我需要一直将计算任务发送到 GCD 队列。我阅读并尝试了很多,并创建了我的第一个解决方案,我认为该解决方案有效,但效果不佳:
class ViewModel: ObservableObject, CalculatorDelegate
var calculator = Calculator()
@Published var input1: Date
@Published var input2: Int
@Published var input3: Bool
init()
calculator.delegate = self
$input1.sink [weak self] in
self?.calculator.requestUpdate()
.store(...)
$input2.sink [weak self] in
self?.calculator.requestUpdate()
.store(...)
$input3.sink [weak self] in
self?.calculator.requestUpdate()
.store(...)
func createRequest() -> Data.Request return Data.Request(input1, input2, input3)
func setResult(result: Data) ...
还有计算器本身:
class Calculator
var delegate: CalculatorDelegate?
private static let queue = DispatchQueue(label: "asdf", qos: .userInteractive)
private let blInstance = BL()
private var reqForUpdate = false
func register(for requestProvider: CalculatorDelegate)
self.delegate = requestProvider
func requestUpdate()
if !reqForUpdate
reqForUpdate = true
scheduleUpdate()
private func scheduleUpdate()
Calculator.queue.async [weak self] in
if let request = self?.delegate?.createRequest()
self?.reqForUpdate = false
let result = self?.blInstance.calc(request: request)
if let result = result
DispatchQueue.main.async
self?.delegate?.setResult(result: result)
protocol CalculatorDelegate
func createRequest() -> Data.Request
func setResult(result: Data)
这是一个好的策略吗?我认为它太复杂了,它似乎没有我上面描述的Android版本那么高效(有一个无休止的运行计算后台线程等待新数据或只要有新数据就可以处理)。此外,GCD 是否适合在这里使用?我不确定...
我简化了代码以强调重要部分。非常感谢您的帮助或提示。
【问题讨论】:
Java 线程只是封装工作单元的抽象。它不是连续运行的。它只是封装状态并在有工作要做时调度执行。在 ios 上,您可以将工作分派到 GCD 队列上,或者您可以使用 OperationQueue 甚至只是一个组合管道。你能解释一下你的计算是如何运作的吗?它会在新数据到来之前完成吗?如果新数据在完成之前到达,您是否需要能够中断/重新启动计算? 【参考方案1】:你问:
因此,不可能让后台线程运行、暂停并等待新输入并计算它们 - 对吗?
GCD 做的事情与此非常相似:它有一个“工作线程”池,当您将某些内容分派到队列时,它只是抓取一个可用的工作线程来运行分派的代码。这消除了启动新线程的开销。这产生了一种高性能的多线程机制。
GCD 将开发人员从管理线程中抽象出来,并避免不必要地创建和销毁线程。只需使用 GCD(或操作队列或async
-await
)即可享受高效的线程利用率。
GCD 是否适合在这里使用?
GCD 有效,但我也会考虑async
-await
并发系统或操作队列。
具体来说,一个关键的设计要求是您希望能够取消您的计算(这样您就可以开始另一个计算)。 GCD 的 DispatchWorkItem
支持取消,但操作队列和 async
-await
可以说更优雅地处理这个问题。因此,虽然您可以使用 GCD,但如果我的目标操作系统(例如 iOS 13 及更高版本)支持,我个人建议async
-await
,如果我需要支持旧操作系统,请使用操作队列。
但无论是调度队列、操作队列,还是async
-await
,你都会想参与“合作取消”。具体来说,您将让您的耗时计算定期检查相应的isCancelled
属性,如果true
则退出计算。
@IBOutlet weak var calculateButton: UIButton!
@IBOutlet weak var cancelButton: UIButton!
@IBOutlet weak var label: UILabel!
private var task: Task<Void, Error>?
@IBAction func didTapCalculate(_ sender: Any)
...
task = Task.detached
try await self.calculate()
await MainActor.run [weak self] in
...
self?.task = nil
@IBAction func didTapCancel(_ sender: Any)
task?.cancel()
task = nil
func calculate() async throws
...
repeat
// check to see if task has been canceled
try Task.checkCancellation() // or `if Task.isCancelled return `
// perform iteration of calculations here
// now update the UI if necessary
if shouldUpdateUI
Task [value] in
await updateLabel(with: value)
while shouldKeepCalculating
@MainActor
func updateLabel(with value: Double) async
label.text = ...
【讨论】:
【参考方案2】:您使用 Combine 的 ObservableObject
和 sink
作为输入是非常不标准的。仅当您想将组合管道的输出assign
到@Published
时才应使用此方法,因此这里的方法不正确。我推荐一个标准的 SwiftUI 方法,比如下面这个异步计算器示例。注意@Binding
和.task(id:)
的使用。
let formatter: NumberFormatter =
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
()
struct CalculatorView: View
@State var calc = Calculator()
var body: some View
InputsView(calc: $calc)
OutputsView(calc: calc)
struct Calculator: Equatable
var input1: Int = 0
var input2: Int = 0
func calculate() async -> Int
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
return input1 + input2
struct InputsView: View
@Binding var calc: Calculator // gives write-access to the inputs
var body: some View
HStack
Text("Input 1")
TextField("Text", value: $calc.input1, formatter: formatter)
HStack
Text("Input 2")
TextField("Text", value: $calc.input2, formatter: formatter)
struct OutputsView: View
let calc: Calculator // gives read access to the inputs and body is called when they change.
@State var result = 0
var body: some View
HStack
Text("Result")
Text("\(result)")
.task(id: calc) // runs on appear and when calc inputs changes, automatically cancelled if already started and on disappear.
result = await calc.calculate()
【讨论】:
嗨@malhal!非常感谢您的回复!这似乎是一个很棒的 Swift(UI)y 解决方案。虽然,我使用的是定义为ObservableObject
的 ViewModel,将这些输入变量保存为 Published
属性。整个应用程序取决于他们。对我来说,将这些输入变量打包成一个结构(代码中的Calculator
)似乎不是“最佳”解决方案。否则,我会在 UI 中单独访问它们。在 UI 中为所有三个输入变量添加三次 task(id: viewModel.varX) ...
是否有问题?当然,最初计算会进行 3 次...
如果在有新值并开始新值之前任务尚未完成,则该任务被取消。但是,一直支持取消异步任务是一项挑战,您需要做的是让它只执行一次完整的计算,而不是三个。
啊,我忘了,谢谢!您是否还有其他想法,我如何将这三个变量分开放在 ViewModel 中,而不是将它们全部打包在一个结构中?那将完全改变代码,我不确定我是否仍然能够单独收听它们。其次,我不确定是否还有比这个(伟大的)更好的解决方案,因为我的计算总是使用当前值,我希望计算总是完成(以获得中间值)。如果输入变量发生变化,它应该在完成后重新计算。
另一种方法是使其成为模型而不是视图模型。然后可以使用 ObservableObject 并且您也可以使用 Combine 来处理数据,例如 Publishers.CombineLatest($input1, $input2, $input3)。以上是关于iOS / Swift(UI)始终运行的后台计算线程的高效实现-GCD案例?的主要内容,如果未能解决你的问题,请参考以下文章
iOS 14,Swift UI 2 更改 NavigationLink 内选定列表项的背景颜色
即使应用程序在后台运行,如何使用 swift 在 ios 中获取位置更新