Objective-C块实现更好的异步控制流

37

我正在使用AFNetworking进行对Web服务的异步调用。其中一些调用必须被链接在一起,即调用A的结果被调用B使用,B的结果又被调用C使用,等等。

AFNetworking通过在创建操作时设置成功/失败块来处理异步调用的结果:

NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/public_timeline.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
    NSLog(@"Public Timeline: %@", JSON);
} failure:nil];
[operation start];

这导致嵌套的异步调用块变得难以阅读。当任务不相互依赖而必须并行执行,并且执行取决于所有操作的结果时,它甚至更加复杂。

似乎更好的方法是利用 promises 框架来清理控制流程。

我发现了 MAFuture,但无法确定如何最好地将其与 AFNetworking 集成。由于异步调用可能具有多个结果(成功/失败)并且没有返回值,因此它似乎不是一个理想的选择。

任何指针或想法都将不胜感激。


谢谢您的提问 - 您得到了一些很好的答案。不过,我最初找到它时遇到了一些麻烦,并通过查看承诺来到这里。这种反模式可能会发生在任何异步回调API中:它并不特定于AFNetworking。我使用了类似于“序列化嵌套块回调”的搜索。也许一些更多的标签可以帮助?不过这可能只是我自己的问题! :-) - Benjohn
6个回答

19

我为此创建了一个轻量级的解决方案。它被称为Sequencer,可以在github上找到。

它使得链式API调用(或任何其他异步代码)易于理解和使用。

以下是使用AFNetworking的示例:

Sequencer *sequencer = [[Sequencer alloc] init];

[sequencer enqueueStep:^(id result, SequencerCompletion completion) {
    NSURL *url = [NSURL URLWithString:@"https://alpha-api.app.net/stream/0/posts/stream/global"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        completion(JSON);
    } failure:nil];
    [operation start];
}];

[sequencer enqueueStep:^(NSDictionary *feed, SequencerCompletion completion) {
    NSArray *data = [feed objectForKey:@"data"];
    NSDictionary *lastFeedItem = [data lastObject];
    NSString *cononicalURL = [lastFeedItem objectForKey:@"canonical_url"];

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:cononicalURL]];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        completion(responseObject);
    } failure:nil];
    [operation start];
}];

[sequencer enqueueStep:^(NSData *htmlData, SequencerCompletion completion) {
    NSString *html = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding];
    NSLog(@"HTML Page: %@", html);
    completion(nil);
}];

[sequencer run];

1
这是一个漂亮、整洁、简单的解决方案。谢谢分享。 - Ben Clayton
看起来如果第1步或第2步出现错误,剩下的步骤就不会被执行。 - fabb
@fabb 我相信这是期望的结果 - 这确实是我想要实现的效果。 - Benjohn
@fabb :-) 老实说,我不确定 Promise 是什么。这里的 Sequencer 实际上只是你想要执行的任务队列(确切地说)。它的主要便利之处在于它会“考虑”下一个任务是什么(通过从队列前部弹出它),并提供了一个简单的接口来向下一步传递参数。 - Benjohn
@Benjohn 我强烈建议你学习 Promise。我知道两个非常好的 iOS 库,它们都有出色的文档和 Promise 的介绍:https://github.com/couchdeveloper/RXPromise 和 http://promisekit.org/。 - fabb
显示剩余5条评论

10

我尚未使用过,但听起来Reactive Cocoa正是设计用于实现您所描述的功能。


1
我已经使用过它,Jon说得没错。它非常适合这种事情。 - Chris Devereux
有趣。我之前了解过Reactive Cocoa,但没有考虑过在这种情况下使用它。由于AF操作都符合KVO,我可以将处理程序添加到操作队列或单个操作中。我会试试这个方法。 - bromanko
1
我喜欢ReactiveCocoa的方法。我的博客文章解释了如何使用ReactiveCocoa来实现这个目的。 - Richard H Fung

10

在Gowalla中使用AFNetworking时,在成功块中链接调用并不罕见。

我的建议是尽可能地将网络请求和序列化因素化为您模型中的类方法。然后,对于需要进行子请求的请求,可以在成功块中调用这些方法。

另外,如果您还没有使用它,AFHTTPClient极大地简化了这些复杂的网络交互。


感谢@mattt。那基本上就是我现在正在做的事情。嵌套块只是有一种代码气味。这与深度嵌套的条件逻辑给我的感觉相同。也许我渴望一些像node.js和其他Javascript框架所提供的清洁度,以实现更可读的函数式编程。 - bromanko
1
深度嵌套并不是这种方法的固有结果——通过将回调有效地分解为它们自己的方法,它应该看起来更像函数式语言中的链接。不过,必须深入超过两个嵌套调用肯定是有问题的,这可能意味着您应该考虑创建一个新的API调用,以一次性获取您所需的所有内容(如果您有这样的权力)。 - mattt

6

PromiseKit可能会很有用。它似乎是最受欢迎的Promise实现之一,其他人已经编写了类别将其与库集成,如AFNetworking,参见PromiseKit-AFNetworking


4
这里在Github上有一个CommonJS风格的Objective-C实现的Promise库:https://github.com/mproberts/objc-promise 以下是示例(摘自Readme.md):
Deferred *russell = [Deferred deferred];
Promise *promise = [russell promise];

[promise then:^(NSString *hairType){
    NSLog(@"The present King of France is %@!", hairType);
}];

[russell resolve:@"bald"];

// The present King of France is bald!

我还没有尝试过这个库,但是看起来很“有前途”,尽管这个例子稍微有些不足(抱歉,我忍不住了)。


看起来它可能非常有用,但它不符合ARC标准,我也没有能力去做 {叹气}。 - mpemburn
1
这个提交似乎使它符合ARC标准:https://github.com/mproberts/objc-promise/commit/9bdeac0d6b1305f00c9c3e4c64bef2743536ed9a - eremzeit

0
您可以将NSBlockOperationsemaphore结合使用来实现它:
- (void)loadDataByOrderSuccess:(void (^)(void))success failure:(void (^)(void))failure {
    // first,load data1
    NSBlockOperation * operation1 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData1Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }];
    // then,load data2
    NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData2Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    }];
    // finally,load data3
    NSBlockOperation * operation3 = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        [self loadData3Success:^{
            dispatch_semaphore_signal(sema);
        } failure:^{
            !failure ?: failure();
        }];
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        !success ?: success();
    }];
    [operation2 addDependency:operation1];
    [operation3 addDependency:operation2];
    NSOperationQueue * queue = [[NSOperationQueue alloc] init];
    [queue addOperations:@[operation1, operation2, operation3] waitUntilFinished:NO];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接