测试已测试 | ✓ |
Lang语言 | Objective C++Objective C++ |
许可 | Apache 2 |
发布最后发布 | 2016 年 9 月 |
由Andreas Grosam维护。
在 Objective-C 中提供了 Promises/A+ 规范的线程安全实现以及扩展。
如果您喜欢更现代的“Skala-like” futures and promise 库,该库以 Swift 实现,您可以查看 FutureLib。
有关重大更改和 API 扩展,请参阅变更日志文档。
采用异步 "非阻塞" 风格
支持链式后续处理
支持取消
通过错误传播简化错误处理
线程安全实现
处理器可以在各种执行上下文中执行,例如 dispatch_queue
、NSThread
、NSOperationQueue
和 NSManagedObjectContext
。
RXPromise
对象轻量级。
RXPromise
受到 Promises/A+ 规范的启发,该规范定义了 JavaScript 中承诺的开放标准实现。
大部分的功劳归功于他们的工作和基于他们的工作的人们的智慧!
有关安装说明,请参阅:安装
一般来说,Promise 表示异步任务的最终结果,或者是在任务失败时的错误原因。类似和相同的概念也被称为 future、deferred 或 delay(也请参见维基百科文章: Futures and promises)。
《RXPromise》实现旨在最大程度地满足《[Promises/A+ 规范](https://github.com/promises-aplus/promises-spec)》中指定的要求。该规范最初是为JavaScript语言编写的,但其架构和设计几乎可以在任何语言中实现。
异步非阻塞
《RXPromise》采用异步非阻塞风格。也就是说,调用点可以调用一个异步任务,该任务立即返回一个《RXPromise》对象。例如,启动异步网络请求
RXPromise* usersPromise = [self fetchUsers];
异步方法fetchUsers
立即返回,其最终结果将由返回的对象,即promise
对象表示。
给出一个promise,调用点可以通过注册一个`Continuation`来“登记”以获取结果和定义错误原因,以及结果可用时如何继续程序。
基本上,“Continuation”是一个完成处理程序和错误处理程序,它们是提供结果和错误作为参数的代码块。拥有一个promise,可以随时设置一个或多个continuation。
通过注册一个continuation,可以通过RXPromise
的三个属性之一来实现:`then`、`thenOn`或`thenOnMain`,并提供了处理块的定义
usersPromise.then(^id(id result){
NSLog(@"Users: %@", result);
return nil;
},
^id(NSError* error){
NSLog(@"Error: %@", error);
return nil;
});
有关“Continuation”的更详细解释,请参见本章理解Promise和在调用点使用Promise。
线程安全
《RXPromise》的主要方法都是异步和线程安全的。这意味着,使用《RXPromise》的特定实现将类似于一个纯异步而且线程安全的系统,其中没有任何线程会被阻塞。(有一些例外,其中某些零散的方法确实会阻塞)。
显式执行上下文
“执行上下文”定义了Continuation(更准确地说,完成处理程序或错误处理程序)最终将在哪里执行。为《RXPromise》设置continuation时,我们可以使用`thenOn`和`thenOnMain`属性显式指定执行上下文。执行上下文用于确保并发处理共享资源的需求,这些资源在处理程序和其他地方将被并发访问。执行上下文可以是调度队列、`NSOperationQueue`、`NSThread`甚至`NSManagedObjectContext`。
参见执行上下文。
取消
此外,《RXPromise》还支持取消,这对于几乎所有实际应用都是极其宝贵的。有关取消的更多详细信息,请参阅本章取消。
一组辅助方法
该库还提供了一些有用的辅助方法,使得管理一系列或一组异步操作变得更加容易。请参阅源文档。
毫无疑问,我们的编码风格将越来越异步。Cocoa API已有大量异步方法提供完成处理程序,也有许多通过代理方法支持异步编程风格的框架。
然而,要正确处理异步问题是困难的,尤其是在问题变得更加复杂时。
有了Promise,解决异步问题就更容易了。这使得利用已经采取异步风格的框架和API变得简单直接。给定的实现看起来像是同步的,但解决方案仍然完全是异步的。代码也变得更简洁,并且由于采用了Blocks,它大大提高了整个异步问题的本地性,因此代码对其他人来说也更容易理解和跟随。
想象一下,我们的目标是在以下六个步骤中实现所述的任务
JSON
。JSON
响应。saveWithChildContext:
(以下说明)异步将它们保存到持久存储中。假设,我们已实现了以下 异步 方法
在视图控制器中
/**
Performs login on a web service. This may ask for user credentials
in a separate UI.
If the operation succeeds, fulfills the returned promise with @"OK",
otherwise rejects it with the error reason (for example the user
cancelled login, or the authentication failed on the server).
*/
- (RXPromise*) login;
/**
Perform a network request to obtain a JSON which contains "Objects".
If the operation succeeds, fulfills the returned promise with a
`NSData` object containing the JSON, otherwise rejects it with the
error reason.
*/
- (RXPromise*) fetchObjects;
实际应用程序也会使用Core Data来管理持久存储。我们的 "Core Data Stack" 非常标准,有一个在主线程上执行的 "main context",其父级是一个在私有队列上运行并具有后端存储的 "root context"。假设这个Core Data堆栈已经设置好了,我们实现以下方法
/**
Saves the chain of managed object contexts starting with the child
context and ending with the root context which finally writes into
the persistent store.
If the operation succeeds, fulfills the returned promise with the
childContext object, otherwise rejects it with the error reason.
*/
- (RXPromise*) saveWithChildContext:(NSManagedObjectContext*)childContext;
有了这些方法,以下 单条语句 异步执行了上述六个步骤中定义的复杂任务
RXPromise* fetchAndSaveObjects =
[self login]
.then(^id(id result){
return [self fetchObjects];
}, nil)
.then(^id(NSData* json){
NSError* error;
id jsonArray = [NSJSONSerialization JSONObjectWithData:json
options:0
error:&error];
if (jsonArray) {
NSAssert([jsonArray isKindOfClass:[NSArray class]]); // web service contract
return jsonArray; // parsing succeeded
}
else {
return error; // parsing failed
}
}, nil)
.then(^id(NSArray* objects){
// Parsing succeeded. Parameter objects is an array containing
// NSDictionaries representing a type "object".
// Create managed objects from the JSON and save them into
// Core Data:
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = self.coreDataStack.managedObjectContext;
for (NSDictionary* object in objects) {
// note: `createWithParameters:inManagedObjectContext` executes on
// the context's queue
[Object createWithParameters:object inManagedObjectContext:moc];
}
// Finally, asynchronously save the context to the persistent
// store and return the result (a RXPromise):
return [self.coreDataStack saveWithChildContext:moc];
}, nil)
.thenOnMain(^id(id result){
// Update our UI
self.objects = [Object allInManagedObjectContext:self.coreDataStack.mainContext];
[self.tableView reloadData];
return @"OK";
}, nil)
.then(nil, ^id(NSError* error){
// If something went wrong in any of the above four steps, the error
// will be propagated down and "caught" in this error handler.
// Just log it to the console (we could show an alert view, or
// display the error info in a status line, too):
NSLog(@"Error: %@", error);
// We return "nil" in order to indicate that we "handled" the error
// and can proceed normally:
return nil;
});
上述代码是一个单一且复杂的语句,它本身是异步的,由多个异步任务组成,形成一个 连续的任务链。
控制和数据流应该很容易识别:步骤是从上到下执行的。我们从一个异步方法开始,设置一个后续操作,即使用 then(<completion-handler, error-handler>)
注册一个完成处理程序和错误处理程序。我们还可以看到处理程序块可以是 nil
。
整个连续的任务链的 最终 结果是通过承诺 fetchAndSaveObjects
表示的。
如果任何异步任务失败或任何处理程序返回一个 NSError
对象,错误将在最后的连续操作中 "捕获",这是唯一一个定义了错误处理程序的连续操作。
承诺 fetchAndSaveObjects
还可以用于 取消 整个过程或选择性的 "承诺分支"。如果任务中实现了 "正向取消",则取消承诺还会取消其下层的异步任务(创建此承诺的任务)。
RXPromise
的取消功能可以提供对要取消的 "承诺分支" 的细粒度控制。对于更多详细信息,请参阅 取消。
在接下来的章节中,我们将更深入地探讨承诺。
如前所述,承诺 表示 异步任务的 最终 结果。有一个对异步任务的 调用者(或调用位置),它对最终结果感兴趣,还有一个执行此值的 异步任务。两者通过承诺对象进行通信。
因此,承诺有两个 不同的方面 我们可以查看。一方面是 "Promise API",由 调用位置 使用,另一方面是底层任务使用的 "Resolver API"。
对于每个方面,在类 RXPromise
中都定义了相应的API。
承诺,更确切地说,"根承诺" 通常由底层任务创建。最初,承诺处于 "pending" 状态。这意味着结果尚未可用。此承诺将被立即返回给调用位置。
此承诺将由底层异步任务创建。此承诺的初始状态是 pending。
底层任务将创建并返回其承诺,如下所示
- (void) task {
RXPromise* promise = [[RXPromise alloc] init];
dispatch_async(queue, ^{
// evaluate the result
...
}
return promise;
}
当基础任务最终完成工作并获得结果时,它必须使用该结果来履行其承诺。否则,如果任务失败,它必须使用失败原因来拒绝其承诺。因此,解决承诺要么是履行,要么是拒绝。
基础任务必须最终解决承诺。
履行或拒绝承诺可以按照以下方式完成
objective-c
if (result) {
[promise fulfillWithValue:result];
}
else {
[promise rejectWithReason:error];
}
*) 注意,承诺也可以被取消。可以从任何地方发送cancel
消息。取消是一种特殊的拒绝承诺的形式(也见取消)。因此,cancel
消息也可以解决承诺。尽管如此,任何返回承诺的异步任务都必须实现上述规则。特别是,RXPromise
实现要求承诺最终必须得到解决。
当承诺被解决时,其状态将从“挂起”转换为“已解决”或“已拒绝”。一旦承诺被解决,进一步尝试解决它将没有任何效果。也就是说,一旦承诺被解决,其结果以及相应的错误原因和状态就不会再改变。
承诺只能被解决一次,其状态变为已解决或已拒绝。解决承诺后,其状态变得不可变。
解决器的主体API将包括几个方法
- (instancetype)init;
- (void) fulfillWithValue:(id)value;
- (void) rejectWithReason:(id)reason;
在解决器位置使用Promise中定义了更详细的 resolver API 和其用法。
另一方面,调用位置想要处理最终的结果,并一旦结果可用,就继续程序。
为了实现这一点,我们使用then
、thenOn
或thenOnMain
属性,这将在后面简要解释。为了更好地了解调用位置将使用的承诺,我们首先看一下相应的异步方法
带有完成处理器的异步方法
typedef void (^completion_t)(NSArray* users, NSError* error);
-(void) fetchUsersWithParams:(NSDictionary*)params
completion:(completion_t)completion;
完成处理器必须由调用位置定义。完成处理器也被称为“延续”。延续简单地指的是在基础任务完成后应该执行的代码。
另一方面,基础任务的职责是最终在完成后调用完成处理器(传递结果或错误给完成处理器)。在调用位置,程序然后使用完成处理器中定义的代码继续执行。
例如
[self fetchUsersWithParams:params
completion:^(NSArray* users, NSError*error){
if (users) {
dispatch_async(dispatch_get_main_queue(), ^{
self.users = users;
[self.tableView reloadData];
});
}
else {
NSLog(@"Error: %@", error);
}
}];
具有Promise的对应该异步方法如下
使用Promise的异步风格
-(RXPromise*) fetchUsersWithParams:(NSDictionary*)params;
此方法返回一个RXPromise
对象,但没有用于完成处理器的参数。由于它是异步的,因此《RXPromise》对象会立即返回。
返回的承诺代表基础任务的最终结果。返回后,这个承诺(最可能)还没有“解决”,这意味着基础任务仍在忙于评估结果。
乍一看,我们看不出当异步方法完成后如何获取结果并继续执行程序,以及我们如何处理可能发生的错误。但是,不用担心,所有这些都可以通过返回的承诺实现
Promises/A+ 规范建议使用“then”方法来指定延续。延续有两个处理程序,即“完成处理程序”和“错误处理程序”。
也就是说,“延续”将通过承诺来定义。在 RXPromise
中,我们有三种变体,即 then
、thenOn
和 thenOnMain
属性,以建立延续和定义完成处理程序和错误处理程序。
给定表达式 [self fetchUsersWithParams:params]
返回一个承诺,上述示例可能如下所示
[self fetchUsersWithParams:params]
.thenOnMain(^id(id result{
// result is an NSArray representing a JSON Array containing Users
self.users = result;
[self.tableView reloadData];
return nil;
}, ^id(NSError* error){
NSLog(@"Error: %@", error);
return nil;
});
调用位置通过
then
、thenOn
或thenOnMain
属性注册了一个 延续 并定义了完成处理程序和错误处理程序,以便获取底层任务的结果或错误,并继续程序。这被称为 延续。
以下章节将详细介绍如何使用 RXPromise
。
与异步任务相关的相关 API 只是这些方法
- (instancetype)init;
- (void) fulfillWithValue:(id)result;
- (void) rejectWithReason:(id)reason;
此外,还有一些方便的方法
+ (instancetype) promiseWithResult:(id)result;
+ (RXPromise *)promiseWithTask:(id(^)(void))task;
+ (RXPromise *)promiseWithQueue:(dispatch_queue_t)queue task:(id(^)(void))task;
异步任务(可能是一个 NSOperation
或异步传递的块)是一个对象,其生命周期延续到完成任务并且结果可用的时候。
基本上,异步任务的职责如下
承诺(更确切地说,是 根承诺)通常由“异步任务”或包装该任务对象的辅助方法创建。并且通常,承诺最初处于 待定 状态。然而,在某些情况下,创建一个已经解析的承诺是有意义的。RXPromise
提供了适合这些使用场景的 API。
方法 init
将创建一个“待定”承诺。因此,大多数异步任务会创建一个这样的承诺
RXPromise* promise = [[RXPromise alloc] init];
可以使用类辅助方法 promiseWithResult:
创建一个 已解析承诺。承诺是否变为 满足 或 拒绝 取决于参数的类型:如果参数是 NSError
类型,它将被拒绝,否则它将被满足
RXPromise* fulfilledPromise = [RXPromise promiseWithResult:@"OK"];
RXPromise* rejectedPromise = [RXPromise promiseWithResult:error];
当“异步结果提供者”最终成功或失败时,它必须 解析 它关联的承诺。解析就是用相应的值,如最终结果或错误来满足或拒绝承诺。
这意味着,当任务成功时,异步任务必须发送一个 fulfillWithValue:
消息给承诺,其参数将是异步函数的结果。
[promise fulfillWithValue:@"OK"];
注意:参数 value 可以是任何对象或 nil
,但不能是 NSError
对象。将 NSError
对象作为 value 参数传递是未定义的行为。
否则,如果异步函数失败,异步任务必须发送一个 rejectWithReason:
消息给承诺,其参数将是失败的原因,可能是一个 NSError
对象。
例如
if ([users count] == 0) {
[NSError errorWithDomain:@"User"
code:-100
userInfo:@{NSLocalizedFailureReasonErrorKey:@"there are no users"}];
[promise rejectWithReason:error];
}
错误原因“应该”是一个描述性的 NSError
对象。然而,我们也可以使用简短的形式
if ([users count] == 0) {
[promise rejectWithReason:@"there are no users"];
}
在这里,rejectWithReason:
将在内部创建一个 NSError
对象,域为 @"RXPromise"
,错误码为 -1000,userInfo
字典设置如下
userInfo = @{NSLocalizedFailureReasonErrorKey: reason ? reason : @""};
返回的承诺可以随时从其他地方取消。
现在,假设底层异步任务是可取消的,我们还需要准备取消返回的 promise 并将取消消息 转发 到底层异步任务。例如,发送一个 取消
消息。
这可以通过为返回的promise设置一个带错误处理器的延续来实现,该处理器执行异步任务的取消。
以下是如何实现一个异步方法,该方法使用 NSOperation
的子类作为底层“异步结果提供者”的示例:
- (RXPromise*) doSomethingAsync
{
RXPromise* promise = [[RXPromise alloc] init];
__weak RXPromise* weakPromise = promise;
MyOperation* op = [MyOperation alloc] initWithCompletionHandler: ^(id result){
[weakPromise fulfillWithValue:result];
}
errorHandler:^(NSError* error){
[weakPromise rejectWithReason:error];
}];
[self.queue addOperation:op];
// Forward cancellation: Cancel the operation if the returned promise
// has been cancelled:
promise.then(nil, ^id(NSError*error){
[op cancel];
return nil;
});
return promise;
}
几点注意事项
此实现避免了通过使用捕获在异步任务完成处理器中的 __weak
认证指针来保留返回的promise。使用 __weak
引用返回的promise通常是一种推荐的做法。这种样式在更复杂的用例中有许多优点。
因为底层任务是 NSOperation
的子类,它是可取消的,所以 doSomethingAsync
方法设置了一个带错误处理器的延续。当调用的位置取消promise或异步任务失败时,将调用此错误处理器。因此,操作将接收一个 取消
消息。如果操作失败,并且操作的完成处理器拒绝其promise,则该“取消”处理程序也将被调用 - 但由于它已经完成,它对操作没有影响。我们可以通过检查错误属性和promise的状态来更严格地采取行动。
在更复杂的情景中,为特定的异步任务类子类化一个专门的 RXPromise
是有意义的
cancel
方法,将取消转发到任务(它反过来取消promise)使用此子类,假设任务只弱引用返回的promise,我们可以实现如果返回promise没有“订阅者”,任务会“自动”取消,也就是说,一个或多个客户端取消他们从在根promise上注册延续获取的promise并已释放。
这对于更复杂的用例非常有用,在这种情况下,可能有一个重负载且运行时间长的任务,可能有几个观察者,应该取消这个任务 - 但只有当没有观察者对任务的结果感兴趣时。
如 理解Promise 中所述,调用位置可以通过设置延续来获取异步任务的结果,并在任务完成时继续执行
延续
then(<completion-handler>, <error-handler>)
thenOn(<execution-context>, <completion-handler>, <error-handler>)
thenOnMain(<completion-handler>, <error-handler>)
延续包括 完成处理器 、 错误处理器 和 执行上下文 。它将使用 then
、 thenOn
或 thenOnMain
属性注册到promise。
处理器可以是 nil
,但是在一个特定的延续中至少定义一个处理器是有意义的。
执行上下文定义了处理程序在哪里执行。它可以是派发队列、 NSThread
、 NSOperationQueue
或 NSManagedObjectContext
。执行上下文可以是隐式的(使用 then
)或明确的(使用 thenOn
或 thenOnMain
)。
执行上下文(execution context)的作用将在《执行上下文》中进行更详细的解释。
在 RXPromise
库中,我们有三种形式来建立继续(Continuation),可以使用 then
、thenOn
或 thenOnMain
属性。这些属性是如何实现的将在稍后进行更详细的解释。目前,我们只需要知道如何 使用 它们,如下所示
then(<完成处理器>, <错误处理器>)
示例
promise.then(^(id result){
// do something with the result
return completion_handler_result;
}, ^id(NSError* error){
// do something with the error
return error_handler_result;
});
当承诺(Promise)被 解决 时,要么完成处理器要么错误处理器将被调用。在这里,处理器将在一个私有的 执行上下文 中执行。
thenOn(<执行上下文>, <完成处理器>, <错误处理器>)
允许我们 显式 指定一个执行上下文,该处理器(完成处理器或错误处理器)将在其中执行。
dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", 0);
promise.thenOn(sync_queue, ^(id result){
// executing on the "sync_queue"
...
return completion_result;
}, ^id(NSError* error){
// executing on the "sync_queue"
...
return error_result;
});
thenOnMain(<完成处理器>, <错误处理器>)
是执行在主线程上的便捷方式。它相当于:thenOn(dispatch_get_main_queue(), <完成处理器>, <错误处理器>)
promise.thenOnMain(^(id result){
// executing on the main thread
...
return completion_result;
}, ^id(NSError* error){
// executing on the main thread
...
return error_result;
});
执行上下文(execution context)的作用将在之后的章节《执行上下文》中进行更详细的解释。
在
RXPromise
中,异步任务的一系列操作将通过以下方式定义:
then(<完成处理器>, <错误处理器>)
thenOn(<执行上下文>, <完成处理器>, <错误处理器>)
thenOnMain(<完成处理器>, <错误处理器>)
完成处理器和错误处理器都是 Blocks。处理器可以是 nil
。实际上,两者都可以是 nil
,但这没有太大意义。
我们可以看到,每个处理器都接受一个参数并返回一个值。
例如,给定一个我们在之前某处获得的承诺,这是一个启动网络请求的承诺,我们可以在视图控制器中“观察”它并设置一个继续(Continuation)
- (void) viewDidAppear:(BOOL)animated
if (self.model.fetchAllUsersPromsise && !self.busyIndicator.isAnimating) {
[self.busyIndicator startAnimating];
self.model.fetchAllUsersPromise.thenOn(dispatch_get_main_queue(),
^(id result){
[self.busyIndicator stopAnimating];
return nil;
}, ^id(NSError*error){
[self.busyIndicator stopAnimating];
return nil;
});
}
[super viewDidAppear:animated];
}
完成处理器的签名如下
id (^completion_handler_t)(id result);
完成处理器有一个类型为 id
的参数 result,并返回一个类型为 id
的值。
注意:完成处理器将只与一个非 NSError 对象或可能是 nil
作为参数一起调用。
如果相关的承诺是由异步任务(即“root promise”)创建的,那么参数 result 与底层任务最终的解决方案相同。
否则,完成处理器的结果参数与前一个继续返回的 返回值 相同。这并不重要,无论是完成处理器还是错误处理器——相应的处理器只需简单地返回一个非 NSError 对象,该对象将被传递到这个完成处理器。
前一个处理器还可以返回一个承诺。在这种情况下,这个完成处理器将在承诺被满足时被调用,并且参数 result 包含满足承诺的最终结果。
完成处理函数有一个类型为
id
的参数 result。当被调用时,result 将是关联到承诺的任务的最终结果(如果有的话),否则它是前一个后续程序的返回值。
底层任务应该指定并记录其结果的类型。此外,还应该指定可能出错的情况。
错误处理程序是一个以下签名的块
id (^error_handler_t)(NSError* error);
错误处理程序接收一个 NSError
参数,并返回一个类型为 id
的值。
在承诺中,错误将通过链中的后续后续程序“传递”,直到有一个指定了错误处理程序的后续程序。
这意味着,当错误处理程序被调用时,它的参数 error 是其异步任务返回的错误(如果有的话),否则它是由前一阶段的承诺返回的错误或错误原因。
我们可以这样说,错误处理程序“捕获”了从其任务或同一“链”中前一阶段的某个地方“抛出”的未捕获错误。实际上,错误将会像在 try/catch
子句中处理一样处理。
错误处理程序接收一个
NSError
作为参数。它“捕获”了从其任务(如果有的话)抛出的错误,或者从一个前一阶段返回的错误。
在 RXPromise
中,发生在处理器中的错误将通过简单地返回一个 NSError
对象来通知调用点。异步任务通过用错误原因拒绝他们的承诺来发出错误。
注意
在 Objective-C 中,像往常一样使用关键字
throw
或@throw
或使用方法raise:
来抛出异常给调用点是适当的,将错误信号给一个调用点。实际上,从处理器内部抛出异常会导致崩溃。
另请参阅错误传播章节。
请注意,完成或错误处理程序将最终被调用(如果已定义),因为底层任务必须最终解决其承诺:也就是说,如果任务成功,则调用完成处理程序(如果已定义),如果任务失败,则调用错误处理程序(如果已定义)。否则,如果错误处理程序是 nil
,则错误将传递到下一个后续程序并处理。如果在该后续程序中没有定义错误处理程序,它将被传递到下一个后续程序(如果有的话),依此类推。
完成处理程序或错误处理程序(如果已定义)将最终被调用。
完成处理程序和错误处理程序都应返回一个值。此返回值是处理程序的結果。处理程序甚至可以调用另一个异步方法,该方法本身返回一个承诺,并返回此承诺。这种做法实际上非常常见,是定义“异步任务链”的标准方式。
处理器应当返回一个值
在某些情况下,处理器可能会遇到错误的情况并希望将此信号给调用点,而不是继续。在这种情况下,处理器只需返回一个描述失败细节的 NSError
对象。
如果处理器没有产生有意义或有用的结果,它应该返回 nil
- 或者可能是类似 @"OK" 或 @"Finished" 的东西。返回 nil
不会被考虑为失败。
处理程序应始终返回一个值。该值可以是任何对象,例如
@"OK"
,一个RXPromise
,一个NSError
或者nil
。在执行处理程序期间表示失败的唯一方法是通过返回一个NSError
对象。
处理程序的签名在 《then、thenOn 和 thenOnMain 属性》 中进行了详细描述。
立即结果 是一个非承诺对象。
.then(^id(id users){
User* user = users[0];
return user; // return the first user
}, nil)
如果返回的值不是错误,则返回的对象将成为下一个续件的(如果有)处理程序的 结果 参数。
-(RXPromise*) saveUsers:(NSArray*) users;
...
.then(^id(id users){
return [self saveUsers:users];
}, nil)
如果返回的承诺将 实现,则承诺的结果将成为下一个续件(如果有)处理程序的 结果 参数。否则,如果承诺将 拒绝,则错误将 传播 到以下级别,直到从一个错误处理器那里接收到为止。
NSError
来表示一个错误.then(^id(id users){
if (![users count]) {
return [NSError errorWithDomain:@"User"
code:-100
userInfo:@{NSLocalizedFailureReasonErrorKey:@"users is empty"}];
}
return users[0];
}, nil)
如果返回的对象是 NSError
,则错误将 传播 到以下级别,直到从一个错误处理器那里接收到为止。
.then(nil, ^id(NSError* error){
return @"OK";
}, nil)
nil
.then(nil, ^id(NSError* error){
NSLog(@"Error: %@", error);
return nil;
}); // last continuation
不那么明显的是,表达式
then(...,...)
也返回一个承诺。
假设我们有一个异步方法
- (RXPromise*) fetchUsers;
我们可以设置一个续项,如下所示
[self fetchUsers].then(..., ...)
上面的表达式返回一个承诺,它代表了相应处理程序的返回值,该处理程序在从 [self fetchUsers]
返回的承诺被解决时调用(调用完成 或 错误处理器)
RXPromise* finalResultPromise = [self fetchUsers]
.then(^id(id result){
...
return final_result;
}, ^id (NSError* error){
...
return final_result;
});
注意: 在这里,从 [self fetchUsers]
返回的承诺是一个“未命名的临时”承诺。它不是 finalResultPromise
!
异步任务创建的承诺也称为“根承诺”。根承诺没有父承诺,即它没有作为添加续项的效果创建。
另一方面,从表达式返回的承诺,例如
[self fetchUsers].then(..., ...)
将是一个“子承诺”,其“父”是“根承诺”。
承诺的一个很棒的特性是,它们是“可组合”的。这意味着可以“链式调用”几个异步任务:第一个任务异步计算一个结果,该结果将作为下一个任务的输入,该任务本身异步计算另一个结果,该结果将成为第三个任务的输入,依此类推。
[self taskA]
.then(^id(id resultA){
return [self taskB:resultA]
}, nil)
.then(^id(id resultB){
return [self taskC:resultB]
}, nil)
异步任务的链式调用在链式调用章节中进行了详细描述。
承诺(Promises)的一个显著特性是可以“链式调用”:第一个异步任务异步计算一个结果,这个结果将被第二个任务用作输入,然后第二个任务本身异步计算一个结果,这个结果又将作为第三个任务的输入,如此类推。
技术上,“链式调用”是通过使表达式
then(<完成处理器>, <错误处理器>)
和 thenOn(<执行上下文, <完成处理器>, <错误处理器>)
自身返回一个承诺实现的。
RXPromise* root = asyncA();
RXPromise* child1 = root.then(completion1, nil),
RXPromise* child2 = child1.then(completion2, nil),
现在,root
是child1
的父,而child1
是child2
的父,形成一个“承诺链”。
更简洁的链式调用
RXPromise* child2 = root()
.then(completion1, nil)
.then(completion2, nil);
在上面的简洁形式中,我们只获得了整个链的最终结果的承诺。我们并不明文地获取中间承诺。然而,这些中间承诺确实存在,作为匿名临时对象。
如果我们不想继续执行其他后续调用,或者不想保留这个承诺以满足可能从其他地方取消它的需要,我们甚至可以省略保留最终承诺child2
的引用。
root()
.then(completion1, nil)
.then(completion2, nil);
在上述更为简洁的形式中,我们并不明确保留一个“命名”的承诺 - 我们只是定义了后续调用。尽管如此,我们可以确信,这些后续调用最终会被调用 - 即使我们没有代表最终结果的承诺对象。
注意,函数root()
- 或其背后的异步任务 - 将创建并返回一个承诺,代表异步任务的结果。任务负责最终解决它。
任何子承诺的根承诺可以通过属性root
获得
RXPromise* root = child2.root;
承诺的父承诺可以通过属性parent
获得
RXPromise* child1 = child2.parent;
假设,我们有一个异步网络请求返回一个JSON(用户的数组)
- (RXPromise*) fetchUsers;
我们可以按照以下方式定义后续调用
[self fetchUsers]
.then(^id(id json){
// parse the JSON
NSError* error;
id jsonArray = [NSJSONSerialization JSONObjectWithData:json
options:0
error:&error];
if (jsonArray) {
return jsonArray; // handler succeeded
}
else {
return error; // handler failed
}
}, nil)
.then(nil, ^id(NSError* error){
NSLog(@"Error: %@", error);
return nil;
});
在这里,实际上有两个后续调用,后者只是用来处理错误。推荐的做法是在最后一个后续调用中处理潜在的错误。
在每个后续调用中处理错误并不必要。只有在确实需要在当前的后续调用中处理错误时才有意义 - 例如,可能解决某种错误,然后继续正常执行而没有任何错误。如果我们不能解决这个特定的错误,我们就可以返回原始的错误。
这是一个经典的形式化的链式后续调用,其中前一个任务的结果作为下一个任务的输入。
RXPromise* endResult = [self asyncA]
.then(^id(id resultA) {
return [self asyncB:resultA];
}, nil)
.then(^id(id resultB) {
return [self asyncC:resultB];
}, nil)
.then(^id(id resultC) {
return [self asyncD:resultC];
}, nil)
.thenOn(dispatch_get_main_queue(),^id(id result) {
NSLog(@"Result: %@", result);
return result;
}, ^id(NSError* error) {
NSLog(@"ERROR: %@", error);
return error;
});
上面的代码链式调用了四个异步任务A、B、C和D,然后是一个返回即时结果的最后任务。任务的结果将被用作其后续调用中完成处理器所调用任务的输入。
第一个承诺没有父承诺,可以通过通过调用第一个异步任务[self asyncA]
获得。返回值 - 一个承诺,其父承诺是根承诺 - 将立即用于通过then
属性定义处理器,其完成处理器仅调用下一个异步任务asyncB
[self asyncA]
.then(^id(id resultA) {
return [self asyncB:resultA];
}, nil)
在这里,当任务A成功完成后,A的最后结果将作为参数resultA
传递给完成处理器,然后该处理器将这个结果传递到任务B以异步计算另一个结果。
当任务B完成时,其最后结果将被传递给下一个完成处理器,如此类推,直到没有更多的处理器。
最后的thenOn
最终处理来自任务D返回的结果 - 通过简单地将其记录到控制台。在这里,执行上下文已经被明确定义。也就是说,处理程序在指定的队列(在示例中为主队列)上执行。
在语句结束时,promise endResult将是从最后的 thenOn
返回的promise。当所有任务都成功完成时,endResult的value
将成为最后一个完成处理程序的返回值 - 实际上是最后一个任务D的结果。
如果任何任务失败,endResult的value
将是最后一个错误处理程序的返回值,也是失败任务的错误原因。有关更多信息,请参见错误传播。
请注意,该语句将完全异步执行!但是,实际上,它们将顺序调用。
特定的Promise可以有多个后续操作。这仅仅意味着,当某个promise解决时,将同时启动多个处理程序。这会导致一个“分支”,其中包括一个“基本promise”和一个或多个“子promise”。
例如,给定一个返回根promise的异步任务
RXPromise* rootPromise = [self doSomethingAsync];
RXPromise* childPromise1 = rootPromsise.then(completion_handler1, nil);
RXPromise* childPromise2 = rootPromsise.then(completion_handler2, nil);
RXPromise* childPromise3 = rootPromsise.then(completion_handler3, nil);
当rootPromise
解决时,所有三个后续操作将被同时调用,这将调用处理程序completion_handler1
、completion_handler2
和completion_handler2
。
由于在这个示例中,执行处理程序时所处的执行上下文没有明确指定,因此它们实际上在一个私有并发提交队列上执行,也就是说,它们是并行执行的。
可以通过指定具体的提交队列、NSOperationQueue
或NSThread
来控制处理程序之间的“并发”。也请参见执行上下文。
“子promise”childPromise1
、childPromise2
和childPromise3
具有同一个父promise:rootPromise
。
“子promise”childPromise1
、childPromise2
和childPromise3
互为“兄弟”。
本章更多地关注于“then”、“thenOn”和“thenOnMain”属性的实现细节。
“then”、“thenOn”和“thenOnMain”是RXPromise
类的属性,返回一个块。这些属性声明如下
@property (nonatomic, readonly) then_block_t then;
@property (nonatomic, readonly) then_on_block_t thenOn;
@property (nonatomic, readonly) then_on_main_block_t thenOnMain;
块的签名如下所示
RXPromise* (^then_block_t)(completionHandler_t, errorHandler_t)
RXPromise* (^then_on_block_t)(id, completionHandler_t, errorHandler_t)
RXPromise* (^then_on_main_block_t)(completionHandler_t, errorHandler_t)
这些属性返回一个块 —— 这是一个属性非常不寻常。因为块可以在应用“调用运算符”(这只是一个函数调用样式的语法)时被调用,所以我们有一个从属性返回的块的简写方式,例如
promise.then(completionHandler, errorHandler);
完成处理程序和错误处理程序也是块。它的签名如下所示
id (^completionHandler_t)(id result)
id (^errorHandler_t)(NSError* error)
也就是说,处理程序接受一个参数并返回一个类型为id
的对象(或nil
)。
现在,在定义这些处理程序“内联”时,我们最终可以用这种方式构建一个简短的续集
promise.then(^id(id result){
...
return ...;
}, ^id(NSError*error){
...
return ...;
});
请注意,表达式[promise.then(completionHandler, errorHandler)]
也返回一个新promise!
在一个更直观的例子中
RXPromise* newPromise = promise.then(completionHandler, errorHandler);
newPromise
是promise
的子promise。同样,newPromise
的“父”promise是promise
。newPromise
的值是从处理程序返回的值。处理程序可以返回一个promise,在这种情况下,newPromise
最终值将是返回的promise的最终值。
客户端现在可以定义当异步方法成功或失败时应该发生什么,通过定义上面的相应处理块。处理块可能是 nil
,表示不需要采取任何操作。
在一个延续链中,如果任何异步任务拒绝其承诺或任何处理程序返回错误,则错误将被 向下传播 到实现了错误处理程序的延续处。该错误处理程序“捕获”错误,并可能处理它。完成处理程序将不会被调用。
错误处理程序可以通过简单地返回错误来“重新抛出”错误。然后,错误再次被向下传播到下一个错误处理程序捕获之前。
或者,错误处理程序可以“处理”错误并返回任何其他对象,以继续“正常”操作。
因此,除非有强烈的理由来“处理”错误并采取适当的行动,否则无需定义错误处理程序。
例如
[self asyncA]
.then(^id(id result) {
return [self asyncB:result];
}, nil)
.then(^id(id result) {
return [self asyncC:result];
}, nil)
.then(^id(id result) {
// ... do something with result
return nil;
}, nil),
.then(nil, ^id(NSError* error) {
// handle error
return nil;
});
在上面的延续链中,只有最后一个延续处理错误。如果任务A、B或C中发生错误,并且返回的承诺将被拒绝,则错误最终将在最后一个延续中被“捕获”并处理。
另请参阅 错误处理程序。
执行上下文指定了处理程序将在哪里执行。
RXPromise
的所有方法都是完全线程安全的。在执行实例或类方法时没有限制。
然而,需要当心处理程序块中访问共享资源时的并发。
当使用 then
属性来注册完成和处理程序块时,处理程序最终执行的执行上下文是 私有的。这意味着执行块的线程是实施定义的。实际上,RXPromise
将使用一个私有的 并发 执行上下文。
由此可以推断,如果使用 then
属性来注册处理程序,处理程序将 并发 执行,并且处理器对共享资源的并发访问不能自动保证线程安全。
使共享资源访问线程安全
当执行上下文将通过 thenOn
或 thenOnMain
属性显式指定时,例如在处理程序中,可以轻松地将对共享资源的并发访问变为线程安全的
dispatch_queue_t sync_queue = dispatch_queue_create("sync.queue", NULL);
[self doSomethingAsync].thenOn(sync_queue, completion_block, error_block);
在这里,我们使用了一个专门的双工分发队列作为执行上下文来指定处理程序应该在 哪里 执行。很明显,一个 序列 队列将序列化对共享资源的访问,因此并发访问是安全的。对于并发队列,情况要复杂得多,下面将详细介绍。
在 RXPromise
中,我们不仅可以使用分发队列作为执行上下文,还可以使用以下这些
执行上下文类型
在 RXPromise
中,处理程序可以在以下类型的执行上下文中执行
分发队列
,NSThread
,NSOperationQueue
和NSManagedObjectContext
.分发队列可以是一个 序列 或 并发 队列。
使用 then
属性来设置延续,例如
then(<完成处理器>, <错误处理器>)
,
客户端没有指定执行上下文。
在这种情况下,处理程序将在一个《私有并发调度队列》上执行。更确切地说,处理程序将通过《dispatch_async()`》在《并发》队列上分发。因此,在不同的执行上下文中执行的不同连续处理程序可能会《并行》执行。
执行在私有并发队列上的处理程序不应访问共享资源,因为不能提供同步保证。
如前所述,在设置连续时,使用第二形式“《thenOn》”或第三形式“《thenOnMain》”,我们可以显式指定一个执行上下文
使用《thenOn》我们可以指定任何有效的执行上下文
thenOn(<执行上下文>, <完成处理器>, <错误处理器>)
,
和
thenOnMain(<完成处理器>, <错误处理器>)
,
我们指定主线程。第三种形式与《thenOn(dispatch_get_main_queue(),
以下示例中,我们设置了一个在主队列上执行处理程序的处理程序
通过以下方式从主线程访问共享资源以确保线程安全
id sharedResource = ...;
promise.thenOn(dispatch_get_main_queue(), ^(id result){
// executing on the main thread
[sharedResource foo];
return nil;
}, ^id(NSError* error){
// executing on the main thread
[sharedResource foo];
return nil;
});
功能等效的替代形式是使用《thenOnMain》
id sharedResource = ...;
promise.thenOnMain(^(id result){
// executing on the main thread
[sharedResource foo];
return nil;
}, ^id(NSError* error){
// executing on the main thread
[sharedResource foo];
return nil;
});
《thenOnMain》是执行主线程处理程序的推荐形式。
为了同步对共享资源的并发访问,我们可以显式指定处理程序的执行上下文,例如,通过设置一个专门的《序列》调度队列
dispatch_queue_t sync_queue = dispatch_queue_create("sync.queue", NULL);
id sharedResource = ...;
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
在这里,我们在一个根承诺上设置了两个连续的项目。那些连续的项目在根承诺解决时或多或少同时开始。由于处理程序将在指定的调度队列上序列化执行,这保证了不会发生数据竞争。其他客户端也可以使用《相同的》调度队列,而不会引入并发问题。
使用显式执行上下文,可以定义甚至复杂的具有各种同步要求的场景。因为可以从处理程序内部安全地访问共享资源,设置具有访问共享资源复杂组成任务的步骤就变得简单直接。
请记住,从任何地方和任何时候“链接”到手承诺的处理程序也是可能的。只需使用《thenOn》或《thenOnMain》属性建立连续性,并定义处理程序和执行上下文。如果承诺已经解决,处理程序将立即执行,并具有相同的并发保证。
使用《并发》调度队列需要更多的关注。然而,`RXPromise`在不同的程度上保证了线程安全。
`RXPromise`假设处理程序中将对一个假设的共享资源执行《写》访问。
`RXPromise`假设在处理程序中对共享资源的《写》访问。
例如
dispatch_queue_t sync_queue = dispatch_queue_create("sync.queue", DISPATCH_QUEUE_CONCURRENT);
id sharedResource = ...;
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
现在,当队列是《并发》时,这需要一个《屏障》来保证线程安全。因此,`RXPromise`将使用《dispatch_barrier_async`》函数调用处理程序。
`RXPromise`实际上执行以下操作
if ([result isKindOfClass:[NSError class]) {
dispatch_barrier_async(queue, error_handler(result));
}
else {
dispatch_barrier_async(queue, completion_handler(result));
}
因此,在并发调度队列上执行的处理程序将妥善处理共享资源。然而,使用相同并发调度队列的其他客户端可能不行。
`dispatch_barrier_async`保证对共享资源的写访问是线程安全的。
虽然《dispatch_barrier_async`》为《并发》队列提供了线程安全保证,但当处理程序只会对共享资源执行《读》访问时,它有一个小的性能损失。
当使用 then
设置一个后续处理时,处理程序将在一个私有的并发 Dispatch 队列上执行。
RXPromise* root = taskA();
root.then(^id(id result){
...
return nil;
}, nil);
root.then(^id(id result){
...
return nil;
}, nil);
在上面的代码段中,首先获取“root promise”,并保留其引用。root promise 设置了两个后续处理,其处理程序在私有的并发队列上运行。
其效果是,一旦 root promise 被解析,由于任务 A 可用,它将 并发地执行所有后续处理。未显示处理程序执行什么操作,但特别指出,它们不得访问共享资源。
NSOperationQueue
可以以两种模式运行:作为一个 串行 队列,或者作为一个可以设置并发执行操作数的 并发 队列。
通常,当指定一个操作为 并发 运行的 NSOperationQueue
时,不能保证同步。否则,如果操作队列是串行的,则适用串行 Dispatch 队列的相同规则。
例如,线程安全的访问
NSOperationQueue sync_queue = [[NSOperationQueue alloc] init];
[sync_queue setMaxConcurrentOperationCount:1]; // make it serial
id sharedResource = ...;
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
root.thenOn(sync_queue, ^id(id result) {
...
[sharedResource foo];
return nil;
}, nil);
NSThread
上执行指定 NSThread
对象作为执行上下文要求该线程有一个运行循环。待定
NSManagedObjectContext
的私有队列上执行RXPromise
的后续处理可以执行在 NSManagedObjectContext
的私有队列上。一个小例子演示了此功能
这里,我们假设有一个“Core Data Stack”,它有一个与持久存储相关联的“root context”,在私有队列上执行,以及一个在主线程上执行的“main context”,其父上下文是“root context”。
代码示例首先创建了一个根据在私有队列上执行的主上下文创建的子管理对象上下文。然后它在这个新上下文中创建一个新的管理对象。成功完成此操作后,它检索此上下文中的所有管理对象
// Create a new managed object context executing on a private queue and whose
// managed object context will be a child of the main context of the core
// data stack:
NSManagedObjectContext* context = [self.coreDataStack newManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
// Obtain parameters for initializing a User object:
NSDictionary* userParams = ...; // obtain parameters
// Create and register a managed object of type User with that managed
// object context:
[User createWithParameters:user inManagedObjectContext:context];
// Note: `createWithParameters:inManagedObjectContext` implementation ensures
// that the managed object will be modified running on the execution context
// associated to the managed object context!
// Save the context chain and when finished, fetch all Users into the
// same context:
[[self.coreDataStack saveContextChainWithContext:context]
.thenOn(context, ^id(id result) {
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"User"];
NSError* error;
NSArray* users = [context executeFetchRequest:fetchRequest error:&error];
if (users) {
...
return @"OK";
}
else {
return error;
}
},nil)
上面的代码片段异步执行了与关联执行上下文适合其并发策略的核心数据方法。
内部,RXPromise
将使用 performBlock:
来执行处理程序块。
注意
NSManagedObjectContext
必须使用NSPrivateQueueConcurrencyType
或NSMainQueueConcurrencyType
创建。“线程限制”(例如,
NSConfinementConcurrencyType
)目前不支持。
偶尔,某个承诺的客户端希望在底层任务解决它之前放弃对结果的兴趣。为此目的,存在两个取消方法
- (void) cancel;
- (void) cancelWithReason:(id)reason;
例如,一个视图控制器在其 viewWillAppear:
方法中启动一个网络请求,并将此承诺分配给 ivar
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.fetchUsersPromise = [self fetchUsersWithParams:params];
self.fetchUsersPromise
.thenOnMain(^id(id result){
self.users = result;
self.fetchUsersPromise = nil;
[self.tableView reloadData];
return nil;
}, ^id(NSError*error){
NSLog(@"Error: %@", error);
self.fetchUsersPromise = nil;
return nil;
});
}
现在,请求是挂起的,但用户切换到这个视图并且结果不再严格需要时我们该怎么办?在这种情况下,我们可以重写 viewDidDisappear:
方法并实现如下
- (void) viewDidDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.fetchUsersPromise cancel];
self.fetchUsersPromise = nil;
}
当底层任务实现 转发取消 时,它也会收到一个取消消息,并且网络请求停止。
为了理解如何在 RXPromise
中实现取消,需要了解承诺之间的 关系。
如已在第延续返回Promise章节中提到,一个Promise可能有“父Promise”。
有一个属性可以获取Promise的父节点。
@property (nonatomic, readonly) RXPromise* parent;
parent
可能会返回nil
,这意味着接收者是“根Promise”。
由底层异步任务创建的Promise,该任务还负责解析此Promise,通常不会有一个父节点。这被称为“根Promise”。
“根Promise”没有父节点。
“子Promise”通常在设置延续时创建。它们的父节点将成为延续已注册的Promise。
RXPromise* childPromise = parent.then(..., ...);
assert(childPromise.parent == parent);
延续链将创建相应的父、子及孙Promise链。
RXPromise grandChildPromise = childPromise.then(..., ...);
assert(grandChild.parent == childPromise);
由于一个特定的Promise可能为多个延续注册,它也有多个子节点。
RXPromise* child1Promise = parent.then(..., ...);
RXPromise* child2Promise = parent.then(..., ...);
RXPromise* child3Promise = parent.then(..., ...);
child1Promise
、child2Promise
和child3Promise
是拥有相同父Promiseparent
的兄弟节点。
现在,当我们取消一个Promise时,以下重要的规则适用
- 如果一个Promise收到一个
cancel
消息,它将向所有子节点发送cancel
消息,除非它已经被取消。- 取消Promise不会取消其父节点。
由于取消Promise会导致它向所有子节点发送取消消息,即使它已经解析(即满足或因原因被拒绝,但未取消),取消将传递给它的子节点及其子节点,等等。但只有尚未解决的Promise才会真正被取消,即其状态变为因取消原因而拒绝。已经满足或拒绝的Promise不会改变其状态。
然而,父节点不会收到取消消息。
我们真正用来“导航”Promise树的唯一方式是属性parent
和一个方便的属性root
。
@property (nonatomic, readonly) RXPromise* parent;
@property (nonatomic, readonly) RXPromise* root;
当它是一个根Promise时,属性parent
将返回nil
。
属性root
会遍历父节点直到找到根Promise并返回它。如果没有父节点,它返回self
,因此它自己是根。
没有返回Promise子节点的属性,这是严格不必要的。
有了这些知识,我们可以选择性地取消Promise树的一个“分支”,同时留下父节点不变,可能是因为还有一个我们取消的兄弟节点,我们希望该兄弟节点仍然接收结果。