测试已测试 | ✓ |
语语言 | Obj-CObjective C |
许可证 | MIT |
发布上次发布 | 2017 年 2 月 |
由 Cocoapods Admin、Duncan Lewis、Prachi Gauriar 维护。
Task 是一个简单的 Cocoa 框架,用于表达和执行应用程序的工作流程。使用 Task,您只需要表达工作流程中的每个步骤——称为任务——以及它们的前提任务。之后,该框架将处理以正确顺序并具有适当的并发级别执行步骤的机制,并通知您任务何时完成或失败。它还使取消任务、重试失败的任务以及重新运行先前完成的任务和工作流程变得容易。
Task 1.2 对 Task 进行了更新,以更好地与 Swift 3 工作协同。Objective-C API 已用可空性和泛型说明符进行了标注,并已更新示例项目和 README 以使用 Swift 代替 Objective-C。
开始使用 Task 最容易的方式是使用 CocoaPods。
pod 'Task', '~> 1.2'
您也可以构建它,并将构建产品包含在您的项目中。对于 OS X 和 iOS 8,只需将 Task.framework
添加到您的项目中。对于较旧的 iOS 版本,将 Task 的公共头文件添加到您的头文件搜索路径,并链接到 libTask.a
,这些都可以在项目的构建输出目录中找到。
任务框架使得您的应用工作流程的表达和管理变得简单,并能处理其中的各种任务。框架中存在两种类型的对象:TSKTasks
表示工作流程中的单个步骤,而 TSKWorkflows
表示工作流程本身。任务通过它们所执行的工作和当前状态来定义;工作流程则由它们包含的任务以及它们之间的关系来定义。没有工作流程的任务是没有特别用途的。
虽然 TSKTask
和 TSKWorkflow
与 NSOperation
和 NSOperationQueue
似乎相似,但它们代表了非常不同的概念。操作模拟了工作的 单一执行,而操作队列控制这些执行的顺序和并发。操作不模拟成功和失败的概念,并且不能重试或重新运行。操作基本上是瞬时的:一旦它们完成执行,它们的有用性就结束了。
另一方面,任务模拟了工作的 概念。即使在任务执行后,也可以检查其状态,并且可以重新执行。预期的操作是创建任务,在适当的时机开始执行它们,监视它们的进度,并在必要时重新运行或重试它们。工作流程帮助您组织工作,提供了一个中央对象,该对象描述了需要完成的工作以及必须按什么顺序完成。
要使用任务建模工作流程,您首先需要创建一个工作流程对象。每个工作流程可以初始化一个名称——这在调试时很有用——以及一个在工作流程的任务上运行的操作队列。如果您不提供队列,系统会为您创建一个,这就是下面我们将做的事情。
let workflow = TSKWorkflow(name:"Workflow")
创建工作流程后,您需要创建表示您的工作的任务并将其添加到您的工作流程中。每个任务都是一个 TSKTask
实例。在进一步探索 Task 类层次结构的具体内容之前,让我们看看如何创建具有各种任务配置的工作流程。
最简单的非空工作流程包含一个单独的任务
┌───────────┐ ╔═══════════╗ ┌──────────┐
│ Start │───────>║ A ║───────>│ Finish │
└───────────┘ ╚═══════════╝ └──────────┘
在这个工作流程中,任务 A 是唯一的任务,没有先决条件。创建它是微不足道的。
let workflow = TSKWorkflow("Workflow A")
let taskA: TSKTask = …
workflow.add(taskA, prerequisites: nil)
当我们准备运行工作流程时,我们可以发送 —start
消息给它。这将启动 taskA
,一旦它成功完成,工作流程就会结束。我们可以通过添加第二个任务 B 使得这个工作流程略微复杂,只有当 A 成功完成后,B 才会运行。
┌───────────┐ ╔═══════════╗ ╔═══════════╗ ┌──────────┐
│ Start │───────>║ A ║───────>║ B ║───────>│ Finish │
└───────────┘ ╚═══════════╝ ╚═══════════╝ └──────────┘
在这里,A 的成功完成是运行 B 的先决条件,这可能是由于 B 依赖于 A 的结果或执行副作用。在代码中模拟这种工作流程很简单。
let workflow = TSKWorkflow("Workflow A+B")
let taskA: TSKTask = …
let taskB: TSKTask = …
workflow.add(taskA, prerequisites: nil)
workflow.add(taskB, prerequisites: [taskA])
在执行这个工作流程时,Task.framework 会自动先运行 taskA
,然后在 taskA
成功完成后开始运行 taskB
。但如果 B 不依赖于 A 会怎样?
╔═══════════╗
┌───>║ A ║───┐
│ ╚═══════════╝ │
┌───────────┐ │ │ ┌──────────┐
│ Start │───┤ ├───>│ Finish │
└───────────┘ │ │ └──────────┘
│ ╔═══════════╗ │
└───>║ B ║───┘
╚═══════════╝
我们只需改变上面的代码,使 B 不列出 A 作为先决条件。
let workflow = TSKWorkflow("Workflow AB")
let taskA: TSKTask = …
let taskB: TSKTask = …
workflow.add(taskA, prerequisites: nil)
workflow.add(taskB, prerequisites: nil)
通过这个简单的更改,Task.framework 会并发运行 taskA
和 taskB
。这很简单。现在,假设有一个第三方任务 C,它只能在 A 和 B 都执行完成后运行。
╔═══════════╗
┌──>║ A ║───┐
│ ╚═══════════╝ │
┌───────────┐ │ │ ╔═══════════╗ ┌──────────┐
│ Start │───┤ ├──>║ C ║──────>│ Finish │
└───────────┘ │ │ ╚═══════════╝ └──────────┘
│ ╔═══════════╗ │
└──>║ B ║───┘
╚═══════════╝
这同样简单明了。
let workflow = TSKWorkflow("Workflow AB+C")
let taskA: TSKTask = …
let taskB: TSKTask = …
let taskC: TSKTask = …
workflow.add(taskA, prerequisites: nil)
workflow.add(taskB, prerequisites: nil)
workflow.add(taskC, prerequisites: [taskA, taskB])
当运行时,工作流程将自动并发运行任务 A 和 B,但只有当 A 和 B 都成功完成后才开始 C。如果 A 或 B 中的任何一个失败,C 不会被运行。如果我们改变了我们的工作流程,让 C 依赖于 B,但不依赖于 A,我们得到的工作流程将如下所示
╔═══════════╗
┌────────────>║ A ║─────────────┐
│ ╚═══════════╝ │
┌───────────┐ │ │ ┌──────────┐
│ Start │───┤ ├──>│ Finish │
└───────────┘ │ │ └──────────┘
│ ╔═══════════╗ ╔═══════════╗ │
└──>║ B ║──────>║ C ║───┘
╚═══════════╝ ╚═══════════╝
到现在,你可能已经猜到我们的代码会是什么样的了。
let workflow = TSKWorkflow("Workflow A(B+C)")
let taskA: TSKTask = …
let taskB: TSKTask = …
let taskC: TSKTask = …
workflow.add(taskA, prerequisites: nil)
workflow.add(taskB, prerequisites: nil)
workflow.add(taskC, prerequisites: [taskB])
再次强调,Task.framework负责管理任务的执行机制,以便在前提任务成功完成后,尽可能并发地运行任务。当构建工作流程时,您只需告诉框架需要运行哪些任务以及每个任务的前提条件。当然,框架还需要知道执行任务时应执行哪些代码。让我们接下来看看这一点。
每个任务都是TSKTask
的实例,其工作由实例的main()
方法执行。遗憾的是,TSKTask
是一个抽象类,所以它的main()
方法实际上并不做任何事情。为了创建一个执行实际工作的任务,您要么从TSKTask
派生子类并重写它的main()
方法,要么使用TSKBlockTask
和TSKSelectorTask
,这样您就可以将代码块或方法调用包装在任务中。
如果您需要反复运行执行相同类型工作的任务,则派生子类是有意义的。例如,如果您的应用程序反复将一个图片分解为多个瓦片并随后以同样的方式处理这些瓦片,您可以创建一个名为ProcessImageTileTask
的TSKTask
子类,该子类可以并发地在每个瓦片上执行。您的子类将重写main()
来执行工作,并且在成功的情况下调用它自己的finish(with:)
来表示工作已完成。如果您由于某些错误无法完成处理工作,那么取而代之的是调用fail(with:)
。
class ProcessImageTileTask : TSKTask {
override func main() {
do {
// Process image data
let result = try process(imageData, rect: tileRect)
finish(with: result)
} catch {
fail(with: error)
}
}
…
}
对于较小的单次任务,您可以使用TSKBlockTask
。TSKBlockTask
实例执行一个块以执行其工作。该块接受一个TSKTask
参数,您应该在成功时向其中发送finish(with:)
以及失败时发送fail(with:)
。下面的块任务执行一个假想的API请求,并在成功和失败的API请求块中分别调用finish(with:)
和fail(with:)
。
func setUpWorkflow() {
…
let blockTask = TSKBlockTask(name: "API Request") { [weak self] task in
weak?.execute(request, success: { response in
task.finish(with: response)
}, failure: { error in
task.fail(with: error)
})
}
workflow.add(blockTask, prerequisites: [requestTask])
…
}
我们可以同样创建一个使用TSKSelectorTask
执行选择器的任务。选择器接受一个TSKTask
参数。如你所想象,此方法必须在成功时调用finish(with:)
以及失败时调用fail(with:)
。在下面的例子中,我们创建选择器任务并将它的前提设置为上面的blockTask
。在我们的任务方法中,我们读取前提任务的结果并使用该结果作为工作输入。
func setUpWorkflow() {
…
let mappingTask TSKSelectorTask(name: "Map API Result",
target: self,
selector: #selector(mapRequestResult(from:)))
workflow.add(mappingTask, prerequisites: [requestTask])
…
}
…
@objc func mapRequestResult(from task: TSKTask) {
guard let jsonResponse = task.anyPrerequisiteResult as? [String:Any] else {
task.fail(with: …)
return
}
do {
let mappedObjectID = try map(jsonResponse, into: managedObjectContext)
task.finish(with: mappedObjectID)
} catch {
task.fail(with: error)
}
}
同样,该任务实际做的工作是虚拟的,但您已经懂了。
还有两个内置的TSKTask
子类:TSKExternalConditionTask
和TSKSubworkflowTask
。前者的实例不执行任何实际工作,而是在满足某些外部条件之前阻止工作流程中进展。这对于需要一些用户输入才能运行的任务来说是个好主意。例如,假设一个REST API调用需要一个作为参数的用户数据。您可以将API调用表示为一个TSKTask
,并创建一个外部条件任务作为前提
let inputTask = TSKExternalConditionTask(name: "Get input")
workflow.add(inputTask, prerequisites: nil)
let requestTask: TSKTask = …
workflow.add(requestTask, prerequisites: [inputTask])
…
// When the user has entered in their input
inputTask.fulfill(with: userSuppliedData)
在这个例子中,当外部条件任务被满足时,API请求任务自动启动。
TSKSubworkflowTask
是一个将整个工作流程作为其工作单元的任务。这在组合由几个更简单的结构组成的复杂工作流程时非常有用。
let imageWorkflow = TSKWorkflow(name: "Upload Image")
let imageAvailableTask = TSKExternalConditionTask()
let filterWorkflow = workflow(for: imagefilter)
let filterTask = TSKSubworkflowTask(subworkflow: filterWorkflow)
let uploadImageTask = UploadDataTask()
imageWorkflow.add(imageAvailableTask, prerequisites: nil)
imageWorkflow.add(filterTask, prerequisitesTasks: [imageAvailableTask])
imageWorkflow.add(uploadImageTask, prerequisites: [filterTask])
当任务成功完成后,它们可以提供一个结果——一个表示它们工作最终结果的对象。非常常见的是,任务使用它们的前提任务的结果来执行更多工作。例如,一个执行RESTful API调用的工作流可能包括一个发送HTTP请求并将响应字节转换为JSON的任务,随后是一个将第一个任务的结果JSON对象映射为模型对象的任务。Task.framework提供了许多方法来访问任务的先前结果。
在最简单的情况下,任务完全不使用其依赖项的结果;任务只是运行它的 main()
方法则不依赖其依赖项的输出。稍有复杂的情况是,当任务只有一个依赖项并依赖于其结果时。在这种情况下,任务可以简单地对其自身调用 anyPrerequisiteResult()
来获取其某个依赖项的结果。由于任务只有一个依赖项,这相当于直接获取其单一依赖项的结果。
任务也可以统一聚合其依赖项的结果。例如,一个工作流程可能将数据集拆分为块,然后在不同的任务中分别处理这些块,最后在一个最终任务中组合这些任务的结果。在这种情况下,任务可以对其自身调用 allPrerequisiteResults()
来获取所有依赖项结果组成的数组,并统一处理。
有时,任务需要以不同的方式使用多个依赖项的结果。为此,Task.framework 有 键控依赖项 的概念。键控依赖项允许任务为其依赖项分配唯一的键,以便之后可以引用。任务可以使用 TSKWorkflow.add(_:keyedPrerequisites:)
或 TSKWorkflow.add(_:prerequisites:keyedPrerequisites:)
定义它们的键控依赖项。在两种情况下,keyedPrerequisites
参数都是一个将键映射到相关任务的字典。可以使用 prerequisiteResult(forKey:)
发送任务来检索给定键控依赖项的结果。
例如,假设任务被添加到工作流程中,如下所示:
workflow.add(task, keyedPrerequisites: ["userTask": task1, "addressTask": task2])
任务可以轻易地引用 task1
和 task2
的结果,例如在其 main()
方法中,如下所示:
override func main() {
guard let user = prerequisiteResult(forKey: "userTask") as? User,
let address = prerequisiteResult(forKey: "addressTask") as? Address else {
…
}
user.address = address
…
}
此外,如果任务没有某些键控依赖项就无法执行,它可以通过覆盖 requiredPrerequisiteKeys
来指定这一点。我们上面的示例中的 TSKTask
子类可能会像这样覆盖该方法:
var requiredPrerequisiteKeys: Set<AnyHashable> {
return ["userTask", "addressTask"]
}
如果子类覆盖了此方法并返回一个非空集合,则 TSKWorkflow
将确保当任务添加到工作流程时,已存在所需的依赖项键对应的任务。为了方便起见,TSKBlockTask
和 TSKSelectorTask
可以在初始化期间设置其所需的依赖项键。
一旦您已设置任务工作流程,您可以通过发送工作流程 start()
消息来启动执行。这将找到工作流程中所有没有依赖项任务的任务并启动它们。如果您随后想要取消任务(或整个工作流程),可以向其发送 cancel()
消息。重新尝试失败的任务就像发送 retry()
消息一样简单,如果您希望重置成功完成的任务以便可以重新运行它,则发送 reset()
消息。如前所述,任务将向其依赖项传播这些消息,依此类推,因此,例如,取消任务也会取消其所有依赖项任务。
每个任务都有一个可选的代理,它可以在成功、失败或取消时通知它。如果您对整个工作流程的这些事件感兴趣,您可以成为工作流程的代理。工作流程代理会在工作流程中的所有任务都完成以及单个任务失败或取消时接收消息。
Task.framework 函数库已经做了全面文档说明,如果需要了解某个类如何工作,可以查看类头文件。此外,Example-iOS
子目录中包含一个比较复杂的示例,其中包括自定义 TSKTask
子类、外部条件以及任务工作流代理方法。特别是,WorkflowViewController.initializeWorkflow()
是一个很好的地方来尝试自己的任务工作流程配置,其进度可以通过运行示例应用程序来可视化。
如果您想帮助修复错误或向 Task 添加功能,请向我们发送 pull request!
我们使用 GitHub 问题跟踪系统处理错误、增强请求以及我们提供的有限支持,因此为任何这些问题创建一个跟踪项。
所有代码均在 MIT 许可协议下发布。随您使用。