管理多个块调用的最佳方法

cro*_*dor 1 objective-c nsoperationqueue grand-central-dispatch ios objective-c-blocks

我正在开发一个应用程序,当它开始执行时,它必须从webService获取一些数据,类别,加载图像(有时会更改),信息"如何使用"(也可以在服务器中更改,客户端规范.. ).为了获得这些数据,我调用了一些像这样的方法(我有四个类似的方法,每个我需要的一个方法):

-(void) loadAppInfo
{
    __weak typeof(self) weakSelf = self;
    completionBlock = ^(BOOL error, NSError* aError) {
        if (error) {
            // Lo que sea si falla..
        }
        [weakSelf.view hideToastActivity];

    };

    [self.view makeToastActivity];
    [wpNetManager getApplicationInfoWithCompletionBlock:completionBlock];
}
Run Code Online (Sandbox Code Playgroud)

在我的网络管理器中,我有类似这样的方法:

- (void)getApplicationInfoWithCompletionBlock:(CompletionBlock)completionBlock
{
    NSString * lang   = @"es";//[[NSLocale preferredLanguages] objectAtIndex:0];
    NSString *urlWithString = [kAPIInfoScreens stringByAppendingString:lang];
    NSMutableURLRequest *request = nil;
    request = [self requestWithMethod:@"GET" path:urlWithString parameters:nil];

    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [self registerHTTPOperationClass:[AFHTTPRequestOperation class]];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        // Print the response body in text
        NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseObject  options:kNilOptions error:nil];
        NSDictionary *informations = [json objectForKey:kTagInfoSplash];
        if([json count]!= 0){
            for (NSDictionary *infoDic in informations) {
                Info *info = [Info getInfoByTitle:[infoDic objectForKey:kTagInfoTitle]];
                if (info) {
                    //  [User updateUserWithDictionary:dic];
                } else {
                    [Info  insertInfoWithDictionary:infoDic];
                }
            }
            [wpCoreDataManager  saveContext];
        }

        if (completionBlock) {
            completionBlock(NO, nil);
        }

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Error Registro: %@", error);
        if (completionBlock) {
            completionBlock(YES, error);
        }

    }];
    [self enqueueHTTPRequestOperation:operation];
}
Run Code Online (Sandbox Code Playgroud)

所以我要做的是在viewDidLoad中调用这些方法:

[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];
Run Code Online (Sandbox Code Playgroud)

所以,而不是逐个调用这个方法.我想我可以使用GCD来管理这个,同时调用一个加载图像,直到一切都完成,然后调用下一个ViewController.我相信这是一个很好的解决方案吗?如果是问题是我不知道如何添加一些块到gcd.

我试图这样做,而不是称他最后四种方法ViewDidLoad.但它很奇怪:

-(void)myBackGroundTask
{
    [self.view makeToastActivity];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self loadAppInfo];
        [self loadCountriesFromJson];
        [self loadCategoriesFromWS];
        [self loadSplashDataFromWS ];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.view hideToastActivity];
            [self nextController];
        });
    });

}
Run Code Online (Sandbox Code Playgroud)

[self nextController]在我保存所有内容之前调用方法Core Data,我有错误..

Cou*_*per 5

由于你所有的四种方法

[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];
Run Code Online (Sandbox Code Playgroud)

异步的,应该清楚为什么声明

[self nextController];

这四种方法完成之前执行.对?

因此,有完成处理程序在异步方法完成被调用.太糟糕了,你的异步方法都没有完成处理程序.;)

解决问题的关键似乎是异步方法的完成处理程序:

typedef void (^completion_t)(id result, NSError* error);

- (void) loadAppInfo:(completion_t)completionHandler;
- (void) loadCountriesFromJson:(completion_t)completionHandler;
- (void) loadCategoriesFromWS:(completion_t)completionHandler;
- (void) loadSplashFromWS:(completion_t)completionHandler;
Run Code Online (Sandbox Code Playgroud)

看来,要启动所有四大异步方法同时.

如何以及何时调用语句[self nextController]取决于对上述四种异步方法的最终结果的调用是否存在任何依赖关系.

例如,您可以声明:

A. 成功完成后[self nextController]应执行loadAppInfo:.所有其他异步方法都无关紧要.

解决方案如下所示:

    [self loadAppInfo:^(id result, NSError*error){
        if (error == nil) {
            [self nextController];
        }
    }];
    [self loadCountriesFromJson:nil];
    [self loadCategoriesFromWS:nil];
    [self loadSplashFromWS:nil];
Run Code Online (Sandbox Code Playgroud)

如果上述声明仅依赖于其中一种方法,则解决方案非常明显且简单.当你有这样的要求时,它将立即变得更加复杂:

B. [self nextController]当所有四个异步方法成功完成(或多于一个,并且所有其他方法都不相关)时,应执行B.

有几种方法可以解决这个问题.一种方法是使用调度组,信号量和一些状态变量和调度队列来确保并发.但是,这是非常详细的,最终会导致阻塞一个线程,无法取消,并且也非常不理想(除此之外它看起来也是hackish).因此,我不会讨论这个解决方案.


使用NSOperation和依赖关系

另一种方法是利用NSOperation依赖性.这需要将每个异步方法包装到NSOperation子类中.您的方法已经异步,这意味着您在设计子类时需要考虑这一点.

由于只能建立从一个到另一个NSOperation的依赖关系,您还需要为您的语句创建一个NSOperation子类

[self nextController]

需要将其包装到自己的NSOperation子类中.

假设您正确子类化NSOperation,在一天结束时,您将获得五个模块和五个头文件:

LoadAppInfoOperation.h, LoadAppInfoOperation.m,
LoadCountriesFromJsonOperation.h, LoadCountriesFromJsonOperation.m,
LoadCategoriesFromWSOperation.h, LoadCategoriesFromWSOperation.m,
LoadSplashFromWSOperation.h, LoadSplashFromWSOperation.m
NextControllerOperation.h, NextControllerOperation.m
Run Code Online (Sandbox Code Playgroud)

B. NextControllerOperation当所有四个操作成功完成时,应启动B.

在代码中,这看起来如下:

LoadAppInfoOperation* op1 = ...;
LoadCountriesFromJsonOperation* op2 = ...;
LoadCategoriesFromWSOperation* op3 = ...;
LoadSplashFromWSOperation* op4 = ...;

NextControllerOperation* controllerOp = ...;

[controllerOp addDependency:op1];
[controllerOp addDependency:op2];
[controllerOp addDependency:op3];
[controllerOp addDependency:op4];

NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation: op1];
[queue addOperation: op2];
[queue addOperation: op3];
[queue addOperation: op4];
[queue addOperation: controllerOp];
Run Code Online (Sandbox Code Playgroud)

看起来不错?没有?


更有吸引力的方法:承诺

如果这个解决方案NSOperations看起来不太好,太精心设计(五个NSOperation子类!)或者其他什么,这是一个更吸引人的方法,它使用实现Promises的第三方库.

在我解释Promise如何工作以及它们的用途之前(请参阅wiki以获得更一般的描述),我想在此处展示最终代码,并解释如何在以后实现.

披露: 此处的示例代码使用第三方库RXPromise,该库根据Promise/A +规范实现Promise .我是RXPromise图书馆的作者.

在Objective-C中实现了一些Promise库,但无论如何你都可以看看RXPromise;)(参见下面的链接)

关键是创建返回promise的异步方法.假设您的所有方法现在都是异步的,并且具有如下签名:

- (RXPromise*) doSomethingAsync;
Run Code Online (Sandbox Code Playgroud)

然后,您的最终代码将如下所示:

// Create an array of promises, representing the eventual result of each task:

NSArray* allTasks = @[
    [self loadAppInfo],
    [self loadCountriesFromJson],
    [self loadCategoriesFromWS],
    [self loadSplashFromWS]    
]; 
... 
Run Code Online (Sandbox Code Playgroud)

上面的语句是一个非常简短的形式,它启动一些任务并在数组中保存它们的结果对象(一个promise).换句话说,数组allTask​​s包含其任务已经启动并且现在同时运行的promise.

现在,我们继续并定义当此阵列中的所有任务成功完成或任何任务失败时将发生的情况.这里我们使用helper类方法all::

...
[RXPromise all: allTasks]
.then(^id(id results){
    // Success handler block
    // Parameter results is an array of the eventual result 
    // of each task - in the same order
    ...  // do something with the results
    return nil;
},^id(NSError*error){
    // Error handler block
    // error is the error of the failed task
    NSLog(@"Error: %@, error");
    return nil;
});
Run Code Online (Sandbox Code Playgroud)

请参阅上面代码中的注释,以了解成功和错误处理程序(在所有任务完成后调用)如何使用"模糊"进行定义then.

解释如下:


说明:

下面的代码使用RXPromise库.您可以获取GitHub上提供的RXPromise Library的源代码.

还有一些其他实现(SHXPromise,OMPromises等),只需稍加努力就可以将下面的代码移植到其他promise库中.

首先,您需要一个异步方法的变体,如下所示:

- (RXPromise*) loadAppInfo;
- (RXPromise*) loadCountriesFromJson;
- (RXPromise*) loadCategoriesFromWS;
- (RXPromise*) loadSplashFromWS;
Run Code Online (Sandbox Code Playgroud)

请注意,异步方法没有完成处理程序.我们不需要这个,因为返回的对象 - 一个Promise--代表了异步任务的最终结果.任务失败时,此结果也可能是错误.

为了更好地利用承诺的力量,我重构了你原来的方法:

异步任务将创建承诺,并且最终必须通过最终结果fulfillWithValue:或"失败"通过错误来"解析"它rejectWithReason:.请参阅下文,如何RXPromise创建,立即从异步方法返回,并在任务完成或失败后"解决".

在这里,您的方法getApplicationInfo返回一个promise,其最终值将是HTTP响应数据,即NSData包含JSON(或可能是错误):

- (RXPromise*)getApplicationInfo
{
    RXPromise* promise = [[RXPromise alloc] init];
    NSString * lang   = @"es";//[[NSLocale preferredLanguages] objectAtIndex:0];
    NSString *urlWithString = [kAPIInfoScreens stringByAppendingString:lang];
    NSMutableURLRequest *request = nil;
    request = [self requestWithMethod:@"GET" path:urlWithString parameters:nil];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [self registerHTTPOperationClass:[AFHTTPRequestOperation class]];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        [promise fulfillWithValue:responseObject]
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        [promise rejectWithReason:error];
    }];
    [self enqueueHTTPRequestOperation:operation];

    return promise;
}
Run Code Online (Sandbox Code Playgroud)

关于承诺的一些进一步说明:

客户端可以通过使用属性注册处理程序块来分别获取最终结果错误then:

promise.then(<success_handler>, <error_handler>);
Run Code Online (Sandbox Code Playgroud)

处理程序或可选,但您通常设置处理结果的一个或两个.

注意:使用RXPromise你可以注册处理程序块,并在那里你想要的,并尽可能多的,只要你想!RXPromise是完全线程安全的.你只需要在某个地方或需要的时候对这个承诺进行强有力的引用.但是,即使您设置了处理程序,也不需要保留引用.

处理程序块将在专用队列上执行.这意味着,您不知道执行上下文也称执行处理程序的线程,除非您使用此变体:

promise.thenOn(dispatch_queue, <success_handler>, <error_handler>);
Run Code Online (Sandbox Code Playgroud)

这里,dispatch_queue指定将执行处理程序(成功或错误处理程序)的队列.

随后可以执行两个或更多个异步任务(也称为链接),其中每个任务产生结果,该结果成为后续任务的输入.

两种异步方法的"链接"的简短形式如下所示:

RXPromise* finalResult = [self asyncA]
.then(^id(id result){
    return [self asyncBWithResult:result]
}, nil);
Run Code Online (Sandbox Code Playgroud)

这里,asyncBWithResult:只有在asyncA成功完成之后才会执行.上面的表达式返回一个Promise finalResult,它表示asyncBWithResult:结束时"返回" 结果的最终结果,或者包含链中失败的任何任务的错误.

回到你的问题:

您的方法loadAppInfo现在调用异步方法getApplicationInfo以获取JSON数据.当成功时,它会解析它,从中创建托管对象并保存托管对象上下文.它返回一个promise,其值是已保存对象的托管对象上下文:

- (RXPromise*) loadAppInfo {
    RXPromise* promise = [[RXPromise alloc] init];
    [self getApplicationInfo]
    .then(^(id responseObject){
        NSError* err;
        NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseObject  options:kNilOptions error:&err];
        if (json == nil) {
            return err;
        }
        else {
            [wpCoreDataManager.managedObjectContext performBlock:^{
                NSDictionary *informations = [json objectForKey:kTagInfoSplash];
                if([json count]!= 0){
                    for (NSDictionary *infoDic in informations) {
                        Info *info = [Info getInfoByTitle:[infoDic objectForKey:kTagInfoTitle]];
                        if (info) {
                            //  [User updateUserWithDictionary:dic];
                        } else {
                            [Info  insertInfoWithDictionary:infoDic];
                        }
                    }
                    [wpCoreDataManager saveContext]; // check error here!
                    [promise fulfillWithValue:wpCoreDataManager.managedObjectContext];
                }
                else {
                    [promise fulfillWithValue:nil];  // nothing saved
                }
            }];
        }
    }, nil);
    return promise;
}
Run Code Online (Sandbox Code Playgroud)

请注意如何performBlock使用它来确保托管对象与其托管对象上下文的执行上下文正确关联.此外,还使用了异步版本,它非常适合利用promises的解决方案.

重构了这两个方法,它们只执行你想要完成的任务,并且还重构了其他异步方法,这些方法现在返回一个类似上面重构方法的承诺,你现在可以完成你的任务,如开头所示.