我有以下(伪)代码:
- (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也在主线程上。
答案 0 :(得分:19)
如果还在主线程上调用了完成块,则可能很难实现此,因为在完成块可以执行之前,您的方法需要返回。您应该将异步方法的实现更改为:
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:
异步,则输出为:
开始
等待...
完成!
继续
如果方法是同步,则输出为:
开始
完成!
继续
什么是 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];
}];
}
说明:
请参阅?
祝你的项目好运!
答案 5 :(得分:1)
上面有很多很好的通用答案 - 但看起来你要做的就是为使用完成块的方法编写单元测试。在调用块之前,您不知道测试是否已经过去,这是异步发生的。
在我目前的项目中,我正在使用SenTestingKitAsync来执行此操作。它扩展了OCTest,以便在运行所有测试之后,它执行在主运行循环上等待的任何内容并同时评估这些断言。所以你的测试看起来像:
- (void)testAbc
{
[someThing retrieve:@"foo" completion:^
{
STSuccess();
}];
STFailAfter(500, @"block should have been called");
}
我还建议在两个单独的测试中测试someThing
和someObject
,但这与您正在测试的内容的异步性无关。
答案 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
}
}
}];
}
}];