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

时间:2013-07-29 09:18:00

标签: ios objective-c

我有以下(伪)代码:

- (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处等待外部完成块完成?

7 个答案:

答案 0 :(得分:19)

如果还在主线程上调用了完成块,则可能很难实现此,因为在完成块可以执行之前,您的方法需要返回。您应该将异步方法的实现更改为:

  1. 同步。
  2. 使用其他线程/队列完成。然后你可以使用Dispatch Semaphores等待。您初始化值为0的信号量,然后在主线程上调用wait,在完成时调用signal
  3. 无论如何,在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: 异步,则输出为:

      

    开始
      等待...
      完成!
      继续

    如果方法是同步,则输出为:

      

    开始
      完成!
      继续

    什么是 Magic ?调用CFRunLoopRun()不会立即返回,但仅在调用CFRunLoopStop()时才会返回。主RunLoop上的代码,因此再次运行Main RunLoop 将继续执行所有已调度的块,定时器,套接字等。

    警告:可能的问题是,所有其他预定的计时器和块将在此期间执行。此外,如果从未调用完成块,则代码将永远不会到达Continue log。

    你可以将这个逻辑包装在一个对象中,这样可以更容易地重复使用这个模式:

    @interface MYRunLoopSemaphore : NSObject
    
    - (BOOL)wait;
    - (BOOL)signal;
    
    @end
    

    因此代码将简化为:

    MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new];
    [self performOperationWithCompletionOnMainQueue:^{
        [semaphore signal];
    }];
    [semaphore wait];
    

答案 1 :(得分:4)

我认为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();
});

答案 2 :(得分:2)

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];
        }];
    }
}

我刚刚在这里输入了代码,因此某些方法名称拼写错误。

答案 3 :(得分:2)

我目前正在开发一个库(RXPromise,其源代码在GitHub上),这使得许多复杂的异步模式非常容易实现。

以下方法使用类RXPromise并产生100%异步的代码 - 这意味着绝对没有阻塞。 “waiting”将通过在异步任务完成或取消时调用的处理程序完成。

它还使用了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);
}

为了测试最终结果:

[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将完全按照您的描述进行操作 - 除了它是异步。出于测试目的,您可以等到它完成:

  [[self asyncTestAbc].thenOn(...) wait];

但是,你不能在主线程上等待,否则你会遇到死锁,因为asyncTestAbc也会在主线程上调用完成处理程序。


如果您觉得有用,请提供更详细的说明!


注意:RXPromise库仍在“正在进行中”。它可以帮助每个人处理复杂的异步模式。上面的代码使用当前未在GitHub上提交的功能:Property thenOn,其中可以指定将在何处执行处理程序的队列。目前只有属性then省略了运行处理程序的参数队列。除非另有说明,否则所有处理程序都在共享专用队欢迎提出建议!

答案 4 :(得分:1)

这通常是一种阻止主线程的糟糕方法,它会让你的应用无响应,所以为什么不做这样的事呢?

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];
    }];

}

我刚刚使用[UIView动画...]使示例完全正常。只需复制并粘贴到viewcontroller.m并调用[self setup];当然,你应该用你的代码替换它。

或者如果你想:

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. 启动所有内容
  3. 当someThing检索&#34; foo&#34;时会调用一个块,换句话说,它会等待直到someThing检索&#34; foo&#34; (并且主要线程不会被阻止)
  4. 执行块时,调用alterNames
  5. 如果所有项目都在&#34;名称&#34;已经循环,循环&#34;将停止并且C可以被执行。
  6. 否则,查找名称,完成后,用它做一些事情(A),因为它发生在主线程上(你还没有说什么)否则),你也可以在那里做B。
  7. 所以,当A和B完成时,跳回3
  8. 请参阅?

    祝你的项目好运!

答案 5 :(得分:1)

上面有很多很好的通用答案 - 但看起来你要做的就是为使用完成块的方法编写单元测试。在调用块之前,您不知道测试是否已经过去,这是异步发生的。

在我目前的项目中,我正在使用SenTestingKitAsync来执行此操作。它扩展了OCTest,以便在运行所有测试之后,它执行在主运行循环上等待的任何内容并同时评估这些断言。所以你的测试看起来像:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        STSuccess();
    }];

    STFailAfter(500, @"block should have been called");
}

我还建议在两个单独的测试中测试someThingsomeObject,但这与您正在测试的内容的异步性无关。

答案 6 :(得分:0)

 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 
                    }
               }
            }];


        }
    }];