您的方法有一个重要问题:
您的方法populateAssetObject:
是同步方法,它将访问远程资源。此方法将在主线程上执行,因此会阻塞 UI。
您在循环中调用它只会使情况变得更糟。
您真正需要的是一个在后台线程中执行所有操作的异步方法,以及一个在整个操作完成时通知调用站点的完成块:
typedef void (^completion_t)();
- (void) populateAssetsWithURLs:(NSArray*) urls completion:(completion_t)completionHandler;
在你的viewDidLoad
你会这样做:
- (void) viewDidLoad {
[super viewDidLoad];
[self populateAssetsWithURLs:urls ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}];
}
棘手的部分是实现方法populateAssetsWithURLs:completion:
。
一种快速而肮脏的方法,如下所示:
- (void) populateAssetsWithURLs:(NSArray*) urls
completion:(completion_t)completionHandler
{
NSUInteger count = [urls count];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSUInteger i = 0; i < count; ++i) {
[self synchronousPopulateAssetObject:urls[i]];
}
if (completionHandler) {
completionHandler();
}
});
}
- (void) synchronousPopulateAssetObject:(NSString*)url {
...
}
这种方法有几个问题:
它至少阻塞一个线程(但可能更多)只是为了等待结果。这种方法在系统资源方面效率低下 - 但它可能有效。
异步循环
更好的方法是采用异步设计。这种方法的棘手之处在于您有一个异步任务列表( ),每个任务都需要异步启动,并且最终结果也只能异步获得。具有 a非常不适合使循环异步。asynchronousPopulateAssetObject
for loop
因此,您可以想象这样的 API,它可能是以下类别NSArray
:
类别 NSArray:
typedef void (^completion_t)(id result);
-(void) forEachPerformTask:(task_t)task completion:(completion_t)completionHandler;
注意,这task
是一个异步块类型task_t
,有自己的完成处理程序作为参数:
typedef void (^task_t)(id input, completion_t);
该任务将异步应用于数组中的每个元素。当所有元素都处理完毕后,将通过调用forEachPerformTask方法中传递的完成处理程序来通知客户端。
您可以在 GitHub Gist 上找到完整的实现和简短示例:transform_each.m。
很快,我将编辑我的答案并演示一种更优雅的方法,它利用一个帮助库,特别适合解决像这样的异步模式。
但在此之前,我将演示另一种方法,利用NSOperationQueue
:
NSOperationQueue
NSOperationQueue
具有可以取消正在运行和挂起的异步任务的宝贵优势。实际上,执行无法取消的操作列表的异步解决方案是一个不完整的解决方案,并且在大多数情况下可能根本不适合。
此外,NSOperationQueue
可以同时执行其任务。可以使用属性设置发生任务的数量maxConcurrentOperationCount
。
使用 a 时有一些变化NSOperationQueue
,最简单的基本思想是:
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;
for (NSString* url in urls) {
[queue addOperationWithBlock:^{
[self synchronousPopulateAssetObject:url];
}];
}
这里,操作队列被配置为并行运行两个任务。将使用从块动态创建对象addOperationWithBlock:
的便捷方法创建操作。NSOperation
任务将立即在 for 循环中排队。这与 Gist 上显示的“调度方法”不同。在“调度方法”中,只有在前一个任务完成后才会将新任务加入队列。这对系统资源非常友好。
这里的缺点是,无法异步确定所有任务何时完成。不过,有一个“阻塞”解决方案,使用 method waitUntilAllOperationsAreFinished
。由于此方法阻塞了调用线程,并且由于我们想要一个异步方法populateAssetsWithURLs:completion:
,因此我们需要将同步方法包装成异步方法,如下所示:
- (void) populateAssetsWithURLs:(NSArray*) urls
queue:(NSOperationQueue*)queue
completion:(completion_t)completionHandler
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSString* url in urls) {
[queue addOperationWithBlock:^{
[self synchronousPopulateAssetObject:url];
}];
}
[queue waitUntilAllOperationsAreFinished];
if (completionHandler) {
completionHandler();
}
});
}
注意:客户端提供队列。这是因为客户端可以随时发送cancelAllOperations
到队列以停止执行挂起和正在运行的任务。
这里的缺点是,我们需要一个额外的线程,它被阻塞只是为了传递最终结果(可以作为完成处理程序中的参数传递)。
另一个缺点是使用方便的方法时,addOperationWithBlock:
我们没有机会为异步任务指定完成处理程序。
使用方便方法时的另一个缺点addOperationWithBlock:
是我们没有得到NSOperation
设置对其他对象的依赖关系所需的方法(参见NSOperation 官方文档中的 操作依赖关系)。NSOperation
如果我们想充分利用NSOperations
和NSOperationQueue
我们将不得不更加精细。例如,有一个完成处理程序在操作队列处理完所有任务时通知调用站点,这是可行的 - 但它需要设置依赖关系,这需要一个我们需要子类的NSOperation
对象,并且需要执行代码什么以前是简单的异步任务。
尽管如此,依赖功能是无价的,我强烈建议您尝试一下。