如何等待有完成块的方法(全部在主线程上)?

我有以下(伪)代码:

- (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方法的实现更改为:

  1. 同步。
    要么
  2. 使用其他线程/队列完成。 然后你可以使用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]; }]; } 

说明:

  1. 通过调用[self setup]来启动一切;
  2. 当someThing获取“foo”时,块将被调用,换句话说, 它将等到 someThing获取“foo”(并且主线程不会被阻塞)
  3. 当该块被执行时,调用alterNames
  4. 如果“名称”中的所有项目已经循环,“循环”将停止,并且C可以被执行。
  5. 否则,查找名字, 当它完成后 ,用它做一些事情(A),因为它发生在主线程(你没有说别的),你也可以在那里做B。
  6. 所以, 当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一些someThingsomeObject ,但不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 } } }]; } }];