如何等待有完成块的方法(全部在主线程上)?
我有以下(伪)代码:
- (void)testAbc { [someThing retrieve:@"foo" completion:^ { NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"]; for (NSString name in names) { [someObject lookupName:name completion:^(NSString* urlString) { // A. Something that takes a few seconds to complete. }]; // B. Need to wait here until A is completed. } }]; // C. Need to wait here until all iterations above have finished. STAssertTrue(...); }
该代码在主线程上运行,并且完成块A在主线程上。
- 如何在B等待A完成?
- 随后如何在C等待外部完成块完成?
如果您的完成块也在主线程中,则无法实现此function (请参阅下面的编辑),因为要完成执行,您的方法需要先返回。 您需要将asynchronous方法的实现更改为:
- 同步。
要么 - 使用其他线程/队列完成。 然后你可以使用Dispatch Semaphores来等待。 您初始化值为
0
的信号量,然后调用主线程上的wait
并完成signal
。
在任何情况下, 阻止主线程在GUI应用程序中是非常糟糕的主意 ,但这不是你的问题的一部分。
编辑:有一种方法,但可能会有意想不到的后果。 谨慎行事!
主线程是特殊的。 它运行+[NSRunLoop mainRunLoop]
,它也处理+[NSOperationQueue mainQueue]
和dispatch_get_main_queue()
。 所有调度到这些队列的操作或块将在主运行循环内执行。 这意味着,这些方法可能采取任何方法来调度完成块,这应该适用于所有这些情况。 这里是:
__block BOOL isRunLoopNested = NO; __block BOOL isOperationCompleted = NO; NSLog(@"Start"); [self performOperationWithCompletionOnMainQueue:^{ NSLog(@"Completed!"); isOperationCompleted = YES; if (isRunLoopNested) { CFRunLoopStop(CFRunLoopGetCurrent()); // CFRunLoopRun() returns } }]; if ( ! isOperationCompleted) { isRunLoopNested = YES; NSLog(@"Waiting..."); CFRunLoopRun(); // Magic! isRunLoopNested = NO; } NSLog(@"Continue");
这两个布尔值是为了保证块在一致的情况下立即同步完成。
如果-performOperationWithCompletionOnMainQueue:
是asynchronous的 ,则输出将是:
开始
等候…
已完成!
继续
如果该方法是同步的 ,则输出将是:
开始
已完成!
继续
什么是魔术 ? 调用CFRunLoopRun()
不会立即返回,但只有在调用CFRunLoopRun()
才会返回。 此代码位于Main RunLoop上,因此再次运行Main RunLoop将恢复执行所有预定的块,定时器,套接字等。
可能的问题是,所有其他计划的定时器和块将同时执行。 另外,如果完成块没有被调用,你的代码永远不会到达Continue
日志。
你可以把这个逻辑包装在一个对象中,这样可以更容易地使用这个模式:
@interface MYRunLoopSemaphore : NSObject - (BOOL)wait; - (BOOL)signal; @end
所以代码将被简化为:
MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new]; [self performOperationWithCompletionOnMainQueue:^{ [semaphore signal]; }]; [semaphore wait];
我认为Mike Ash (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.html )的答案正好等待完成的几个线程,那么在所有线程完成时都要做些什么,好的是,你甚至可以同步地或者同步地等待使用调度组。
从Mike Ash博客复制并修改一个简短的例子:
dispatch_group_t group = dispatch_group_create(); for(int i = 0; i < 100; i++) { dispatch_group_enter(group); DoAsyncWorkWithCompletionBlock(^{ // Async work has been completed, this must be executed on a different thread than the main thread dispatch_group_leave(group); }); } dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
或者,可以在所有块完成而不是dispatch_group_wait时同步等待并执行一个操作:
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ UpdateUI(); });
int i = 0; //the below code goes instead of for loop NSString *name = [names objectAtIndex:i]; [someObject lookupName:name completion:^(NSString* urlString) { // A. Something that takes a few seconds to complete. // B. i+= 1; [self doSomethingWithObjectInArray:names atIndex:i]; }]; /* add this method to your class */ -(void)doSomethingWithObjectInArray:(NSArray*)names atIndex:(int)i { if (i == names.count) { // C. } else { NSString *nextName = [names objectAtIndex:i]; [someObject lookupName:nextName completion:^(NSString* urlString) { // A. Something that takes a few seconds to complete. // B. [self doSomethingWithObjectInArray:names atIndex:i+1]; }]; } }
我只是在这里键入代码,所以一些方法名称可能拼错了。
我目前正在开发一个库(RXPromise,其源代码位于GitHub上),这使得很多复杂的asynchronous模式很容易实现。
下面的方法使用一个类RXPromise
并产生100%asynchronous的代码,这意味着绝对没有阻塞。 “等待”将通过asynchronous任务完成或取消时调用的处理程序完成。
它还使用了不属于库的一部分的NSArray
类别,但可以使用RXPromise库轻松实现。
例如,你的代码可能看起来像这样:
- (RXPromise*)asyncTestAbc { return [someThing retrieve:@"foo"] .then(^id(id unused /*names?*/) { // retrieve:@"foo" finished with success, now execute this on private queue: NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"]; return [names rx_serialForEach:^RXPromise* (id name) { /* return eventual result when array finished */ return [someObject lookupName:name] /* return eventual result of lookup's completion handler */ .thenOn(mainQueue, ^id(id result) { assert(<we are on main thread>); // A. Do something after a lookupName:name completes a few seconds later return nil; }, nil /*might be implemented to detect a cancellation and "backward" it to the lookup task */); }] },nil); }
为了testing最终的结果:
[self asyncTestAbc] .thenOn(mainQueue, ^id(id result) { // C. all `[someObject lookupName:name]` and all the completion handlers for // lookupName, and `[someThing retrieve:@"foo"]` have finished. assert(<we are on main thread>); STAssertTrue(...); }, id(NSError* error) { assert(<we are on main thread>); STFail(@"ERROR: %@", error); });
方法asyncTestABC
将完全按照你所描述的 – 除了它是asynchronous的 。 出于testing目的,您可以等到它完成:
[[self asyncTestAbc].thenOn(...) wait];
但是,您不能在主线程中等待,否则您会遇到死锁,因为asyncTestAbc
在主线程上调用完成处理程序。
如果您觉得这个有用,请请求更详细的解释!
注意:RXPromise库仍在“正在进行中”。 它可以帮助每个人处理复杂的asynchronous模式。 上面的代码使用了当前没有提交到GitHub上的一个特性:Property thenOn
在哪里可以指定一个队列来执行处理程序。 目前只有属性, then
就省略了处理程序运行的参数队列。 除非另外指定,否则所有处理程序都在共享私有队列上运行 欢迎提出build议!
阻止主线程往往是一个糟糕的做法,它只会让你的应用程序无响应,所以为什么不做这样的事情?
NSArray *names; int namesIndex = 0; - (void)setup { // Insert code for adding loading animation [UIView animateWithDuration:1 animations:^{ self.view.alpha = self.view.alpha==1?0:1; } completion:^(BOOL finished) { names = @[@"John", @"Mary", @"Peter", @"Madalena"]; [self alterNames]; }]; } - (void)alterNames { if (namesIndex>=names.count) { // Insert code for removing loading animation // C. Need to wait here until all iterations above have finished. return; } NSString *name = [names objectAtIndex:namesIndex]; [UIView animateWithDuration:1 animations:^{ self.view.alpha = self.view.alpha==1?0:1; } completion:^(BOOL finished) { name = @"saf"; // A. Something that takes a few seconds to complete. // B. Need to wait here until A is completed. namesIndex++; [self alterNames]; }]; }
我刚刚使用[UIViewanimation…]使例子function齐全。 只需复制并粘贴到您的viewcontroller.m并调用[self setup]; 当然,你应该用你的代码replace它。
或者,如果你想要:
NSArray *names; int namesIndex = 0; - (void)setup { // Code for adding loading animation [someThing retrieve:@"foo" completion:^ { names = @[@"John", @"Mary", @"Peter", @"Madalena"]; [self alterNames]; }]; } - (void)alterNames { if (namesIndex>=names.count) { // Code for removing loading animation // C. Need to wait here until all iterations above have finished. return; } NSString *name = [names objectAtIndex:namesIndex]; [someObject lookupName:name completion:^(NSString* urlString) { name = @"saf"; // A. Something that takes a few seconds to complete. // B. Need to wait here until A is completed. namesIndex++; [self alterNames]; }]; }
说明:
- 通过调用[self setup]来启动一切;
- 当someThing获取“foo”时,块将被调用,换句话说, 它将等到 someThing获取“foo”(并且主线程不会被阻塞)
- 当该块被执行时,调用alterNames
- 如果“名称”中的所有项目已经循环,“循环”将停止,并且C可以被执行。
- 否则,查找名字, 当它完成后 ,用它做一些事情(A),因为它发生在主线程(你没有说别的),你也可以在那里做B。
- 所以, 当A和B完成时 ,跳回到3
看到?
祝你的项目好运!
上面有很多很好的通用答案 – 但是看起来你要做的是为使用完成块的方法编写一个unit testing。 您不知道testing是否已经通过,直到块被调用,这是asynchronous发生的。
在我目前的项目中,我使用SenTestingKitAsync来做到这一点。 它扩展了OCTest,以便在所有testing运行之后,它执行主运行循环中等待的任何内容,并评估这些断言。 所以你的testing可能看起来像:
- (void)testAbc { [someThing retrieve:@"foo" completion:^ { STSuccess(); }]; STFailAfter(500, @"block should have been called"); }
我也build议在两个单独的testing中testing一些someThing
和someObject
,但不pipe你正在testing什么asynchronous性质。
Move B and C to two methods. int flagForC = 0, flagForB = 0; [someThing retrieve:@"foo" completion:^ { flagForC++; NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"]; for (NSString name in names) { [someObject lookupName:name completion:^(NSString* urlString) { // A. Something that takes a few seconds to complete. flagForB++; if (flagForB == [names Count]) { flagForB = 0; //call B if (flagForC == thresholdCount) { flagForC = 0; //Call C } } }]; } }];