10

当使用返回块的方法时,它们会非常方便。但是,当您必须将其中一些串在一起时,它会很快变得混乱

例如,您必须连续调用 4 个 URL:

[remoteAPIWithURL:url1 success:^(int status){
    [remoteAPIWithURL:url2 success:^(int status){
        [remoteAPIWithURL:url3 success:^(int status){
            [remoteAPIWithURL:url2 success:^(int status){
            //succes!!!
            }];
        }];
    }];
}];

因此,对于每次迭代,我都会更深一层,我什至还没有处理嵌套块中的错误。

当存在实际循环时,情况会变得更糟。例如,假设我想上传一个 100 个块的文件:

- (void) continueUploadWithBlockNr:(int)blockNr
{
    if(blocknr>=100) 
    {
    //success!!!
    }
    [remoteAPIUploadFile:file withBlockNr:blockNr success:^(int status)
    {
        [self continueUploadWithBlockNr:blockNr];
    }];
}

这感觉非常不直观,并且很快变得非常难以理解。

在 .Net 中,他们使用 async 和 await 关键字解决了所有这些问题,基本上将这些延续展开为看似同步的流程。

Objective C 的最佳实践是什么?

4

8 回答 8

4

你的问题立即让我想到了递归。事实证明,Objective-c 块可以在递归中使用。所以我想出了以下解决方案,它易于理解并且可以很好地扩展到 N 个任务。

// __block declaration of the block makes it possible to call the block from within itself
__block void (^urlFetchBlock)();

// Neatly aggregate all the urls you wish to fetch
NSArray *urlArray = @[
    [NSURL URLWithString:@"http://www.google.com"],
    [NSURL URLWithString:@"http://www.stackoverflow.com"],
    [NSURL URLWithString:@"http://www.bing.com"],
    [NSURL URLWithString:@"http://www.apple.com"]
];
__block int urlIndex = 0;

// the 'recursive' block 
urlFetchBlock = [^void () {
    if (urlIndex < (int)[urlArray count]){
        [self remoteAPIWithURL:[urlArray objectAtIndex:index] 
            success:^(int theStatus){
                urlIndex++;
                urlFetchBlock();
            }

            failure:^(){
                // handle error. 
            }];
    }
} copy];

// initiate the url requests
urlFetchBlock();
于 2012-11-20T12:56:50.973 回答
2

减少嵌套的一种方法是定义返回单个块的方法。为了促进由 Objective C 编译器通过闭包“自动”完成的数据共享,您需要定义一个单独的类来保存共享状态。

这是如何做到这一点的粗略草图:

typedef void (^WithStatus)(int);

@interface AsyncHandler : NSObject {
    NSString *_sharedString;
    NSURL *_innerUrl;
    NSURL *_middleUrl;
    WithStatus _innermostBlock;
}
+(void)handleRequest:(WithStatus)innermostBlock
            outerUrl:(NSURL*)outerUrl
            middleUrl:(NSURL*)middleUrl
            innerUrl:(NSURL*)innerUrl;

-(WithStatus)outerBlock;

-(WithStatus)middleBlock;

@end

@implementation AsyncHandler

+(void)handleRequest:(WithStatus)innermostBlock
            outerUrl:(NSURL*)outerUrl
            middleUrl:(NSURL*)middleUrl
            innerUrl:(NSURL*)innerUrl {
    AsyncHandler *h = [[AsyncHandler alloc] init];
    h->_innermostBlock = innermostBlock;
    h->_innerUrl = innerUrl;
    h->_middleUrl = middleUrl;
    [remoteAPIWithURL:outerUrl success:[self outerBlock]];
}

-(WithStatus)outerBlock {
    return ^(int success) {
        _sharedString = [NSString stringWithFormat:@"Outer: %i", success];
        [remoteAPIWithURL:_middleUrl success:[self middleBlock]];
    };
}

-(WithStatus)middleBlock {
    return ^(int success) {
        NSLog("Shared string: %@", _sharedString);
        [remoteAPIWithURL:_innerUrl success:_innermostBlock];
    };
}

@end

注意:所有这些都假设 ARC;如果您在没有它的情况下进行编译,则需要Block_copy在返回块的方法中使用。您还需要在下面的调用代码中进行复制。

现在您的原始函数可以在没有“俄罗斯娃娃”嵌套的情况下重写,如下所示:

[AsyncHandler
    handleRequest:^(int status){
        //succes!!!
    }
    outerUrl:[NSURL @"http://my.first.url.com"]
    middleUrl:[NSURL @"http://my.second.url.com"]
    innerUrl:[NSURL @"http://my.third.url.com"]
];
于 2012-11-13T23:00:24.377 回答
2

迭代算法

  • 创建一个__block变量 ( int urlNum) 以跟踪当前 URL(在NSArray其中一个 URL 中)。
  • 让 onUrlComplete 块触发下一个请求,直到所有 URL 都已加载。
  • 触发第一个请求。
  • 加载所有 URL 后,执行“//success!” 舞蹈。

在没有 XCode 帮助的情况下编写的代码(意思是,可能存在编译器错误——必要时会修复):

- (void)loadUrlsAsynchronouslyIterative:(NSArray *)urls {
  __block int urlNum = 0;
  void(^onUrlComplete)(int) = nil; //I don't remember if you can call a block from inside itself.
  onUrlComplete = ^(int status) {
    if (urlNum < urls.count) {
      id nextUrl = urls[urlNum++];
      [remoteAPIWithURL:nextUrl success:onUrlComplete];
    } else {
      //success!
    }
  }
  onUrlComplete(0); //fire first request
}

递归算法

  • 创建一个方法来加载所有剩余的 URL。
  • 当剩余的 URL 为空时,触发“onSuccess”。
  • 否则,触发对下一个 URL 的请求并提供一个完成块,该块递归地调用除第一个剩余 URL 之外的所有方法的方法。
  • 复杂性:我们声明了“onSuccess”块来接受一个int status参数,所以我们向下传递了最后一个状态变量(包括一个“默认”值)。

不借助 XCode 编写的代码(此处为 bug 免责声明):

- (void)loadUrlsAsynchronouslyRecursive:(NSArray *)remainingUrls onSuccess:(void(^)(int status))onSuccess lastStatus:(int)lastStatus {
  if (remainingUrls.count == 0) {
    onSuccess(lastStatus);
    return;
  }
  id nextUrl = remainingUrls[0];
  remainingUrls = [remainingUrls subarrayWithRange:NSMakeRange(1, remainingUrls.count-1)];
  [remoteAPIWithUrl:nextUrl onSuccess:^(int status) {
    [self loadUrlsAsynchronouslyRecursive:remainingUrls onSuccess:onSuccess lastStatus:status];
  }];
}

//fire first request:
[self loadUrlsAsynchronouslyRecursive:urls onSuccess:^(int status) {
  //success here!
} lastStatus:0];

哪个更好

  • 迭代算法简单明了——如果你喜欢玩带有__block变量和作用域的游戏的话。
  • 或者,递归算法不需要__block变量并且相当简单,就像递归算法一样。
  • 递归实现比迭代实现(如已实现)更具可重用性。
  • 递归算法可能会泄漏(它需要对 的引用self),但有几种方法可以解决这个问题:使其成为函数、使用__weak id weakSelf = self;等。

添加错误处理有多容易

  • 迭代实现可以很容易地扩展以检查 的值status,但代价是onUrlComplete块变得更加复杂。
  • 递归实现可能不那么直接扩展——主要是因为它是可重用的。当状态为某某时,是否要取消加载更多 URL?然后传递一个接受int status并返回的状态检查/错误处理块BOOL(例如YES继续,NO取消)。或者也许修改onSuccess以接受两者int status-NSArray *remainingUrls但您需要调用loadUrlsAsynchronouslyRecursive...您的onSuccess块实现。
于 2012-11-17T18:12:09.700 回答
1

您说(在评论中),“异步方法无需使用显式线程即可提供简单的异步性。” 但是您的抱怨似乎是您正在尝试使用异步方法做某事,这并不容易。你看到这里的矛盾了吗?

当您使用基于回调的设计时,您牺牲了直接使用语言的内置结构表达控制流的能力。

所以我建议你停止使用基于回调的设计。Grand Central Dispatch (GCD) 使“在后台”执行工作变得容易(又是那个词!),然后回调到主线程以更新用户界面。因此,如果您有 API 的同步版本,只需在后台队列中使用它:

- (void)interactWithRemoteAPI:(id<RemoteAPI>)remoteAPI {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // This block runs on a background queue, so it doesn't block the main thread.
        // But it can't touch the user interface.

        for (NSURL *url in @[url1, url2, url3, url4]) {
            int status = [remoteAPI syncRequestWithURL:url];
            if (status != 0) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    // This block runs on the main thread, so it can update the
                    // user interface.
                    [self remoteRequestFailedWithURL:url status:status];
                });
                return;
            }
        }
    });
}

因为我们只是使用正常的控制流,所以做更复杂的事情很简单。假设我们需要发出两个请求,然后以最多 100k 的块上传一个文件,然后再发出一个请求:

#define AsyncToMain(Block) dispatch_async(dispatch_get_main_queue(), Block)

- (void)uploadFile:(NSFileHandle *)fileHandle withRemoteAPI:(id<RemoteAPI>)remoteAPI {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        int status = [remoteAPI syncRequestWithURL:url1];
        if (status != 0) {
            AsyncToMain(^{ [self remoteRequestFailedWithURL:url1 status:status]; });
            return;
        }

        status = [remoteAPI syncRequestWithURL:url2];
        if (status != 0) {
            AsyncToMain(^{ [self remoteRequestFailedWithURL:url2 status:status]; });
            return;
        }

        while (1) {
            // Manage an autorelease pool to avoid accumulating all of the
            // 100k chunks in memory simultaneously.
            @autoreleasepool {
                NSData *chunk = [fileHandle readDataOfLength:100 * 1024];
                if (chunk.length == 0)
                    break;
                status = [remoteAPI syncUploadChunk:chunk];
                if (status != 0) {
                    AsyncToMain(^{ [self sendChunkFailedWithStatus:status]; });
                    return;
                }
            }
        }

        status = [remoteAPI syncRequestWithURL:url4];
        if (status != 0) {
            AsyncToMain(^{ [self remoteRequestFailedWithURL:url4 status:status]; });
            return;
        }

        AsyncToMain(^{ [self uploadFileSucceeded]; });
    });
}

现在我确定你会说“哦,是的,看起来很棒。” ;^) 但你也可能会说“如果RemoteAPI只有异步方法,没有同步方法呢?”</p>

我们可以使用 GCD 为异步方法创建同步包装器。我们需要让包装器调用异步方法,然后阻塞直到异步方法调用回调。棘手的一点是,我们可能不知道异步方法使用哪个队列来调用回调,也不知道它是否用于dispatch_sync调用回调。因此,让我们通过从并发队列中调用异步方法来确保安全。

- (int)syncRequestWithRemoteAPI:(id<RemoteAPI>)remoteAPI url:(NSURL *)url {
    __block int outerStatus;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    [remoteAPI asyncRequestWithURL:url completion:^(int status) {
        outerStatus = status;
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_release(sem);
    return outerStatus;
}

更新

我会先回复您的第三条评论,然后再回复您的第二条评论。

第三条评论

您的第三条评论:

最后但并非最不重要的一点是,您使用一个单独的线程来环绕调用的同步版本的解决方案比使用异步替代方案的成本更高。线程是一种昂贵的资源,当它阻塞时,您基本上已经丢失了一个线程。异步调用(至少在 OS 库中的调用)通常以更有效的方式处理。(例如,如果您同时请求 10 个 url,很可能它不会启动 10 个线程(或将它们放入线程池中))

是的,使用线程比仅使用异步调用更昂贵。所以呢?问题是它是否太贵了。在当前 iOS 硬件上的某些场景中,Objective-C 消息过于昂贵(例如,实时人脸检测或语音识别算法的内部循环),但我大部分时间都不会担心使用它们。

线程是否是“昂贵的资源”实际上取决于上下文。让我们考虑一下您的示例:“例如,如果您同时请求 10 个 url,它很可能不会启动 10 个线程(或将它们放入线程池中)”。让我们来了解一下。

NSURL *url = [NSURL URLWithString:@"http://1.1.1.1/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
for (int i = 0; i < 10; ++i) {
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        NSLog(@"response=%@ error=%@", response, error);
    }];
}

所以这里我使用苹果自己推荐的+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]方法异步发送10个请求。我选择了不响应的 URL,因此我可以准确地看到 Apple 使用哪种线程/队列策略来实现此方法。我在运行 iOS 6.0.1 的 iPhone 4S 上运行该应用程序,在调试器中暂停,并截取了 Thread Navigator 的屏幕截图:

10 NSURLConnection sendAsynchronousRequest:线程

您可以看到有 10 个线程标记为com.apple.root.default-priority。我已经打开了其中的三个,因此您可以看到它们只是普通的 GCD 队列线程。每个调用一个定义在 中的块+[NSURLConnection sendAsynchronousRequest:…],它只是转身并调用+[NSURLConnection sendSynchronousRequest:…]. 我检查了所有 10 个,它们都有相同的堆栈跟踪。所以,事实上,操作系统库确实启动了 10 个线程

我将循环计数从 10 增加到 100,发现 GCD 将com.apple.root.default-priority线程数限制为 64。所以我的猜测是我发出的其他 36 个请求在全局默认优先级队列中排队,甚至不会开始执行,直到64 个“正在运行”的请求中的一些完成了。

那么,使用线程将异步函数转为同步函数是否成本太高?我会说这取决于你计划同时做多少。如果这个数字低于 10 甚至 20,我会毫不犹豫。

第二条评论

这让我想到了你的第二条评论:

然而,当你有:同时做这三件事,当它们中的“任何”完成时,然后忽略其余的,同时做这三个调用,当它们全部完成时,就会成功。

在这些情况下,使用 GCD 很容易,但如果您愿意,我们当然可以将 GCD 和异步方法结合起来使用更少的线程,同时仍然使用语言原生工具进行控制流。

首先,我们将为远程 API 完成块创建一个 typedef,以便稍后节省输入:

typedef void (^RemoteAPICompletionBlock)(int status);

我将以与以前相同的方式启动控制流,将其从主线程移至并发队列:

- (void)complexFlowWithRemoteAPI:(id<RemoteAPI>)remoteAPI {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

首先,我们要同时发出三个请求,并等待其中一个成功(或者,大概三个都失败)。

因此,假设我们有一个函数 ,statusOfFirstRequestToSucceed它发出任意数量的异步远程 API 请求并等待第一个成功。此函数将为每个异步请求提供完成块。但是不同的请求可能需要不同的参数……我们如何将 API 请求传递给函数?

我们可以通过为每个 API 请求传递一个文字块来做到这一点。每个文字块接受完成块并发出异步远程 API 请求:

        int status = statusOfFirstRequestToSucceed(@[
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI requestWithCompletion:completion];
            },
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI anotherRequestWithCompletion:completion];
            },
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI thirdRequestWithCompletion:completion];
            }
        ]);
        if (status != 0) {
            AsyncToMain(^{ [self complexFlowFailedOnFirstRoundWithStatus:status]; });
            return;
        }

好的,现在我们已经发出了前三个并行请求,并等待一个成功,或者所有它们都失败。现在我们要再发出三个并行请求并等待所有请求都成功,或者其中一个请求失败。所以它几乎是相同的,除了我要假设一个函数statusOfFirstRequestToFail

        status = statusOfFirstRequestToFail(@[
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI requestWithCompletion:completion];
            },
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI anotherRequestWithCompletion:completion];
            },
            ^(RemoteAPICompletionBlock completion) {
                [remoteAPI thirdRequestWithCompletion:completion];
            }
        ]);
        if (status != 0) {
            AsyncToMain(^{ [self complexFlowFailedOnSecondRoundWithStatus:status]; });
            return;
        }

现在两轮并行请求都完成了,我们可以通知主线程成功了:

        [self complexFlowSucceeded];
    });
}

总的来说,这对我来说似乎是一个非常简单的控制流程,我们只需要实现statusOfFirstRequestToSucceedand statusOfFirstRequestToFail. 我们可以在没有额外线程的情况下实现它们。由于它们非常相似,我们将让它们都调用一个辅助函数来完成真正的工作:

static int statusOfFirstRequestToSucceed(NSArray *requestBlocks) {
    return statusOfFirstRequestWithStatusPassingTest(requestBlocks, ^BOOL (int status) {
        return status == 0;
    });
}

static int statusOfFirstRequestToFail(NSArray *requestBlocks) {
    return statusOfFirstRequestWithStatusPassingTest(requestBlocks, ^BOOL (int status) {
        return status != 0;
    });
}

在辅助函数中,我需要一个队列来运行完成块,以防止出现竞争条件:

static int statusOfFirstRequestWithStatusPassingTest(NSArray *requestBlocks,
    BOOL (^statusTest)(int status))
{
    dispatch_queue_t completionQueue = dispatch_queue_create("remote API completion", 0);

请注意,我只会将块放在completionQueueusing上dispatch_sync,并且dispatch_sync始终在当前线程上运行块,除非队列是主队列。

我还需要一个信号量,以在某些请求以传递状态完成或所有请求都完成时唤醒外部函数:

    dispatch_semaphore_t enoughJobsCompleteSemaphore = dispatch_semaphore_create(0);

我将跟踪尚未完成的作业数量和最后一个要完成的作业的状态:

    __block int jobsLeft = requestBlocks.count;
    __block int outerStatus = 0;

jobsLeft变为 0 时,表示我已设置outerStatus为通过测试的状态,或者所有作业都已完成。这是完成块,我将在其中跟踪我是否完成等待。如果远程 API 并行调度多个完成块(在单独的线程或并发队列上),我将全部执行completionQueue以序列化对jobsLeftand的访问:outerStatus

    RemoteAPICompletionBlock completionBlock = ^(int status) {
        dispatch_sync(completionQueue, ^{

我检查外部函数是否仍在等待当前作业完成:

            if (jobsLeft == 0) {
                // The outer function has already returned.
                return;
            }

接下来,我减少剩余的作业数量,并使已完成作业的状态可用于外部函数:

            --jobsLeft;
            outerStatus = status;

如果已完成作业的状态通过测试,我将设置jobsLeft为零以防止其他作业覆盖我的状态或单选外部函数:

            if (statusTest(status)) {
                // We have a winner.  Prevent other jobs from overwriting my status.
                jobsLeft = 0;
            }

如果没有待等待的作业(因为它们都已完成或因为该作业的状态通过了测试),我会唤醒外部函数:

            if (jobsLeft == 0) {
                dispatch_semaphore_signal(enoughJobsCompleteSemaphore);
            }

最后,我释放了队列和信号量。(保留将在以后,当我遍历请求块来执行它们时。)

            dispatch_release(completionQueue);
            dispatch_release(enoughJobsCompleteSemaphore);
        });
    };

这是完成块的结束。该功能的其余部分是微不足道的。首先我执行每个请求块,并保留队列和信号量以防止悬空引用:

    for (void (^requestBlock)(RemoteAPICompletionBlock) in requestBlocks) {
        dispatch_retain(completionQueue); // balanced in completionBlock
        dispatch_retain(enoughJobsCompleteSemaphore); // balanced in completionBlock
        requestBlock(completionBlock);
    }

请注意,如果您使用 ARC 并且您的部署目标是 iOS 6.0 或更高版本,则不需要保留。

然后我只是等待其中一个作业唤醒我,释放队列和信号量,并返回唤醒我的作业的状态:

    dispatch_semaphore_wait(enoughJobsCompleteSemaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(completionQueue);
    dispatch_release(enoughJobsCompleteSemaphore);
    return outerStatus;
}

请注意,结构statusOfFirstRequestWithStatusPassingTest是相当通用的:您可以传递任何您想要的请求块,只要每个请求块调用完成块并传入一个int状态。您可以修改该函数以处理来自每个请求块的更复杂的结果,或取消未完成的请求(如果您有取消 API)。

于 2012-11-17T20:32:18.473 回答
0

在我自己研究这个的时候,我碰到了一个对 Objective-C 的反应式扩展的端口。反应式扩展就像具有查询一组事件或异步操作的能力。我知道它在 .Net 和 JavaScript 下得到了很大的普及,现在显然也有一个用于 Objective-C 的端口

https://github.com/blog/1107-reactivecocoa-for-a-better-world

语法看起来很棘手。我想知道它是否有用于 iPhone 开发的真实世界经验,以及它是否确实优雅地解决了这个问题。

于 2012-11-14T09:37:11.590 回答
0

我倾向于像您在 NSOperation 的子类中描述的那样包装大嵌套块集群 f**** ,这些子类描述了您的大嵌套块集群 f***实际在做什么(而不是让它们散落在其他代码中)。

例如,如果您的以下代码:

[remoteAPIWithURL:url1 success:^(int status){
    [remoteAPIWithURL:url2 success:^(int status){
        [remoteAPIWithURL:url3 success:^(int status){
            [remoteAPIWithURL:url2 success:^(int status){
            //succes!!!
            }];
        }];
    }];
}];

旨在获得一个授权令牌,然后同步一些东西,也许它会是一个 NSAuthorizedSyncOperation ......我相信你明白了要点。这样做的好处是将漂亮整洁的行为捆绑在一个类中,如果事情发生变化,可以在一个地方编辑它们。我的 2 美分。

于 2012-11-18T20:41:06.257 回答
0

不确定那是不是要你在哪里寻找?尽管数组中的所有对象都需要不同的时间来完成,但它们都按照提交到队列的顺序出现。

typedef int(^SumUpTill)(int);
SumUpTill sum = ^(int max){
    int i = 0;
    int result = 0;
    while (i < max) {
        result += i++;
    }
    return result;
};

dispatch_queue_t queue = dispatch_queue_create("com.dispatch.barrier.async", DISPATCH_QUEUE_CONCURRENT);
NSArray *urlArray = @[  [NSURL URLWithString:@"http://www.google.com"],
                        @"Test",
                        [sum copy],
                        [NSURL URLWithString:@"http://www.apple.com"]
];

[urlArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    dispatch_barrier_async(queue, ^{
        if ([obj isKindOfClass:[NSURL class]]) {
            NSURLRequest *request = [NSURLRequest requestWithURL:obj];
            NSURLResponse *response = nil;
            NSError *error = nil;
            [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
            NSLog(@"index = %d, response=%@ error=%@", idx, response, error);
        }
        else if ([obj isKindOfClass:[NSString class]]) {
            NSLog(@"index = %d, string %@", idx, obj);
        }
        else {
            NSInteger result = ((SumUpTill)obj)(1000000);
            NSLog(@"index = %d, result = %d", idx, result);
        }
    });
}];
于 2012-11-20T15:20:55.257 回答
0

在 NSDocument 中,以下方法可用于序列化:

Serialization
– continueActivityUsingBlock:
– continueAsynchronousWorkOnMainThreadUsingBlock:
– performActivityWithSynchronousWaiting:usingBlock:
– performAsynchronousFileAccessUsingBlock:
– performSynchronousFileAccessUsingBlock:

我只是在深入研究这个,但看起来这将是一个很好的起点。

于 2012-11-20T15:38:36.293 回答