除了@Zhang 的出色回答,我想描述一下 OP 面临的常见问题,以及这个常见问题的“通用解决方案”可能是什么样子。
共同的目标是:
从服务器获取项目列表。每个项目都包含一个指向其他资源(例如图像)的 URL。
收到列表后,对于列表中的每个项目,获取 URL 给出的资源(图像)。
当以同步方式实现这一点时,解决方案很明显,实际上也很容易。然而,当采用异步风格时——这是进行网络时的首选方式——一个可行的解决方案变得异常复杂,除非你知道如何解决这些问题;)
这里有趣的部分是#2。第 1 部分可以通过异步调用和完成函数简单地完成,其中完成函数调用第 2 部分。
为了使事情更容易理解,我将做一些简化和一些先决条件:
在第 #1 部分中,我们获得了一个元素列表,比如一个NSArray
包含我们元素的对象。每个元素都有一个属性,它是另一个资源的 URL。
现在,我们可以轻松地假设我们已经有一个表示 N 个输入值的元素数组,这些元素将在一个循环中异步处理 - 一个接一个。让我们将该数组命名为“源数组”。
我们将处理异步方法/函数。让方法/函数发出信号表明它已完成异步处理的一种方法是完成处理程序(一个块)。
所有完成处理程序的通用签名将定义如下:
typedef void (^completion_t)(id result);
注:result代表异步函数或方法的最终结果。它可能是我们期望的那种东西(例如图像),或者它可能指示错误,例如通过传递和NSError
对象。
为了实现我们的第 2 部分,我们需要一个异步方法/函数,它接受一个输入(来自输入数组的一个元素)并产生一个输出。这对应于您的“获取图像资源”任务。稍后我们需要为我们在第 1 部分中获得的“输入数组”的每个元素应用此方法/函数。
通用函数,一个“转换函数”,将具有以下签名:
void transform(id input, completion_t completion);
相应的方法将具有此签名:
-(void) transformWithInput:(id)input
completion:(completion_t)completionHandler;
我们可以为函数定义一个 typedef,如下所示:
typedef void (^transform_t)(id input, completion_t completion);
请注意,转换函数或方法的结果将通过完成处理程序的参数传递。同步函数只有一个返回值并返回结果。
注意:名称“transform”只是一个通用名称。您可以将您的网络请求包装在一个方法中并获得这种“转换”功能。在 OP 的示例中,URL将是输入参数,而完成处理程序的结果参数将是从服务器获取的图像(或错误)。
注意:这个和下面的简化只是为了让异步模式的解释更容易理解。在实践中,异步函数或方法可能需要其他输入参数,并且完成处理程序也可能具有其他参数。
现在,更“棘手”的部分:
以异步方式实现循环
嗯,这与同步编程风格有点“不同”。
有目的地,我们定义了某种forEach函数或方法来执行此迭代。该函数或方法本身就是异步的!我们现在知道任何异步函数或方法都会有一个完成处理程序。
因此,如果是函数,我们可以如下声明“forEach”函数:
`void transform_each(NSArray* inArray, transform_t task, completion_t completion);`
transform_each
依次将异步变换函数任务应用于输入数组inArray中的每个对象。处理完所有输入后,它会调用完成处理程序completion。
完成处理程序的结果参数是一个数组,其中包含每个变换函数的结果,其顺序与相应输入的顺序相同。
注意:这里的“顺序”意味着输入一个接一个地处理。该模式的一个变体可以并行处理输入。
参数inArray是我们从第 1 步收集的“输入数组”。
参数任务是我们的异步转换函数,它几乎可以是任何接受输入并产生输出的东西。这将是我们在 OP 示例中的异步“获取图像”任务。
参数完成是在处理完所有输入后调用的处理程序。它的参数包含数组中每个变换函数的输出。
transform_each
可以如下实现。首先我们需要一个“助手”功能do_each
。
do_each
实际上是以异步方式实现循环的整个模式的核心,所以你可以在这里仔细看看:
void do_each(NSEnumerator* iter, transform_t task, NSMutableArray* outArray, completion_t completion)
{
id obj = [iter nextObject];
if (obj == nil) {
if (completion)
completion([outArray copy]);
return;
}
task(obj, ^(id result){
[outArray addObject:result];
do_each(iter, task, outArray, completion);
});
}
这里有趣的部分,以及用于实现循环(作为 for_each 函数)的“通用异步模式”或“惯用语”do_each
将从转换函数的完成处理程序中调用。这可能看起来像递归,但实际上并非如此。
参数iter指向数组中要处理的当前对象。它还将用于确定停止条件:当枚举器指向结束时,我们nil
从 method 获得结果nextObject
。这最终会停止循环。
否则,将以当前对象作为输入参数调用变换函数任务。该对象将按照任务的定义进行异步处理。完成后,将调用任务的完成处理程序。它的参数结果将是变换函数的输出。处理程序需要将结果添加到结果数组 outArray中。然后它do_each
再次调用助手。这似乎是一个递归调用,但实际上不是:前者do_each
已经被返回。这只是另一个调用do_each
.
一旦我们有了它,我们就可以简单地完成我们的transform_each
功能,如下所示:
void transform_each(NSArray* inArray, transform_t task, completion_t completion) {
NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
NSEnumerator* iter = [inArray objectEnumerator];
do_each(iter, task, outArray, completion);
}
NSArray 类别
为了方便起见,我们可以使用“forEach”方法轻松地为 NSArray 创建一个类别,该方法按顺序异步处理输入:
@interface NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion;
@end
@implementation NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(transform_t) task completion:(completion_t) completion {
transform_each(self, task, completion);
}
@end
可以在 Gist 上找到代码示例:transform_each
解决常见异步模式的一个更复杂的概念是利用“Futures”或“Promises”。我已经在一个小型库中为 Objective-C 实现了“承诺”的概念:RXPromise。
上面的“循环”可以实现,包括通过 RXPromise取消异步任务的能力,当然还有更多。玩得开心 ;)