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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17920169/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 00:32:50  来源:igfitidea点击:

How to wait for method that has completion block (all on main thread)?

iosobjective-c

提问by meaning-matters

I have the following (pseudo) code:

我有以下(伪)代码:

- (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(...);
}

This code is running on main thread, and also the completion block A is on main thread.

这段代码在主线程上运行,完成块 A 也在主线程上。

  • How do I wait at B for A to complete?
  • How do subsequently wait at C for the outer completion block to complete?
  • 我如何在 B 等待 A 完成?
  • 随后如何在 C 处等待外部完成块完成?

采纳答案by Tricertops

If your completion block is also called on the Main Thread, it might be difficult to achieve this, because before the completion block can execute, your method need to return. You should change implementation of the asynchronous method to:

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

  1. Be synchronous.
    or
  2. Use other thread/queue for completion. Then you can use Dispatch Semaphores for waiting. You initialize a semaphore with value 0, then call waiton main thread and signalin completion.
  1. 保持同步。
    或者
  2. 使用其他线程/队列完成。然后你可以使用 Dispatch Semaphores 进行等待。您使用 value 初始化一个信号量0,然后调用wait主线程并signal完成。

In any case, blocking Main Thread is very bad ideain GUI applications, but that wasn't part of your question. Blocking Main Thread may be required in tests, in command-line tools, or other special cases. In that case, read further:

无论如何,在 GUI 应用程序中阻塞主线程是一个非常糟糕的主意,但这不是您问题的一部分。在测试、命令行工具或其他特殊情况下可能需要阻塞主线程。在这种情况下,请进一步阅读:



How to wait for Main Thread callback onthe Main Thread:

如何在主线程等待主线程回调:

There is a way to do it, but could have unexpected consequences. Proceed with caution!

有一种方法可以做到,但可能会产生意想不到的后果。谨慎行事!

Main Thread is special. It runs +[NSRunLoop mainRunLoop]which handles also +[NSOperationQueue mainQueue]and dispatch_get_main_queue(). All operations or blocks dispatched to these queues will be executed within the Main Run Loop. This means, that the methods may take anyapproach to scheduling the completion block, this should work in all those cases. Here it is:

主线程是特殊的。它运行+[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");

Those two booleans are to ensure consistency in case of the block finished synchronously immediately.

这两个布尔值是为了确保在块立即同步完成的情况下的一致性。

In case the -performOperationWithCompletionOnMainQueue:is asynchronous, the output would be:

如果-performOperationWithCompletionOnMainQueue:异步的,输出将是:

Start
Waiting...
Completed!
Continue

开始
等待……
完成!
继续

In case the method is synchronous, the output would be:

如果方法是同步的,输出将是:

Start
Completed!
Continue

开始
完成!
继续

What is the Magic? Calling CFRunLoopRun()doesn't return immediately, but only when CFRunLoopStop()is called. This code ison Main RunLoop so running the Main RunLoop againwill resume execution of all scheduled block, timers, sockets and so on.

什么是魔法?调用CFRunLoopRun()不会立即返回,而是仅在CFRunLoopStop()被调用时返回。此代码在主RunLoop因此运行主RunLoop再次将恢复所有预定块,定时器,插座等的执行。

Warning:The possible problem is, that all other scheduled timers and block will be executed in meantime. Also, if the completion block is never called, your code will never reach Continuelog.

警告:可能的问题是,所有其他计划的计时器和块将同时执行。此外,如果从不调用完成块,您的代码将永远不会到达Continue日志。

You could wrap this logic in an object, that would make easier to use this pattern repeatedy:

您可以将此逻辑包装在一个对象中,这样可以更轻松地重复使用此模式:

@interface MYRunLoopSemaphore : NSObject

- (BOOL)wait;
- (BOOL)signal;

@end

So the code would be simplified to this:

所以代码将简化为:

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

回答by Paul Fennema

I think that Mike Ash (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.htmlhas exactly the answer to 'waiting for several threads on completion and then do something when all threads are finished'. The nice thing is that you even can wait either synchronously or a-synchronously, using dispatch groups.

我认为 Mike Ash (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.html对“等待多个线程完成并然后在所有线程完成后做一些事情'。好处是你甚至可以使用调度组同步或异步等待。

A short example copied and modified from Mike Ash his blog:

从 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);

Alternatively, you can a-synchronously wait and perform an action when all blocks completed instead of the dispatch_group_wait:

或者,您可以异步等待并在所有块完成时执行操作而不是 dispatch_group_wait:

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    UpdateUI();
});

回答by CouchDeveloper

I'm currently developing a library (RXPromise, whose sources are on GitHub) which makes a number of complex asynchronous patterns quite easy to implement.

我目前正在开发一个库(RXPromise,其来源在 GitHub 上),它使许多复杂的异步模式很容易实现。

The following approach utilizes a class RXPromiseand yields code which is 100% asynchronous - which means, there is absolutely no blocking. "waiting" will be accomplished through the handlers which get called when an asynchronous tasks is finished or cancelled.

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

It also utilizes a category for NSArraywhich is not part of the library - but can be easily implemented utilizing RXPromise library.

它还使用了一个NSArray不属于库的类别- 但可以使用 RXPromise 库轻松实现。

For example, your code could then look like this:

例如,您的代码可能如下所示:

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

In order to test the final result:

为了测试最终结果:

[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);
});

The method asyncTestABCwill exactly do what you have described - except that it's asynchronous. For testing purposes you can wait until it completes:

该方法asyncTestABC将完全按照您的描述执行 - 除了它是异步的。出于测试目的,您可以等到它完成:

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

However, you must not wait on the main thread, otherwise you get a deadlock since asyncTestAbcinvokes completion handler on the main thread, too.

但是,您不能在主线程上等待,否则会因为asyncTestAbc在主线程上调用完成处理程序而导致死锁。



Please request a more detailed explanation if you find this useful!

如果您觉得这有用,请请求更详细的解释!



Note: the RXPromise library is still "work under progress". It may help everybody dealing with complex asynchronous patterns. The code above uses a feature not currently committed to master on GitHub: Property thenOnwhere a queue can be specified where handlers will be executed. Currently there is only property thenwhich omits the parameter queue where handler shall run. Unless otherwise specified all handler run on a shared private queue. Suggestions are welcome!

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

回答by johnyu

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

I just typed the code here, so some methods names might be spelled wrong.

我只是在此处输入代码,因此某些方法名称可能拼写错误。

回答by Leonard Pauli

It's often a bad approach to block the main thread, it will just make your app unresponsive, so why not do something like this instead?

阻塞主线程通常是一种不好的方法,它只会使您的应用程序无响应,那么为什么不做这样的事情呢?

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

}

I have just used [UIView animation...] to make to example fully functional. Just copy and paste into your viewcontroller.m and call [self setup]; Of course, you should replace that with your code.

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

Or if you want:

或者如果你想要:

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

}

Explanation:

解释:

  1. Start everything by calling [self setup];
  2. A block will be called when someThing retrieves "foo", in other words, it will waituntil someThing retrieves "foo" (and the main thread won't be blocked)
  3. When the block is executed, alterNames is called
  4. If all the items in "names" have been looped through, the "looping" will stop and C could be executed.
  5. Else, lookup the name, and when it's done, do something with it (A), and because it happens on the main thread (You haven't said anything else), you could do B there too.
  6. So, when A and B is complete, jump back to 3
  1. 通过调用 [self setup] 开始一切;
  2. 当 someThing 检索到“foo”时会调用一个块,换句话说,它会等待直到 someThing 检索到“foo”(并且主线程不会被阻塞)
  3. 当块被执行时,alterNames 被调用
  4. 如果“names”中的所有项目都已循环通过,则“循环”将停止并且可以执行 C。
  5. 否则,查找名称,当它完成时,用它做一些事情(A),因为它发生在主线程上(你还没有说什么),你也可以在那里做 B。
  6. 所以,当 A 和 B 完成时,跳回到 3

See?

看?

Good luck with your project!

祝你的项目好运!

回答by dpassage

Lots of good general-purpose answers above - but it looks like what you're trying to do is write a unit test for a method that uses a completion block. You don't know if the test has passed until the block is called, which happens asynchronously.

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

In my current project, I'm using SenTestingKitAsyncto do this. It extends OCTest so that after all the tests are run, it executes whatever's waiting on the main run loop and evaluates those assertions as well. So your test could look like:

在我当前的项目中,我使用SenTestingKitAsync来做到这一点。它扩展了 OCTest,以便在所有测试运行后,它会执行主运行循环中等待的任何内容并评估这些断言。所以你的测试可能看起来像:

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

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

I would also recommend testing someThingand someObjectin two separate tests, but that's regardless of the asynchronous nature of what you're testing.

我还建议测试someThing,并someObject在两个独立的测试,但是这是无论你正在测试的异步特性。

回答by arundevma

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


        }
    }];