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

时间:2013-11-20 14:07:51

标签: ios objective-c objective-c-blocks grand-central-dispatch nsoperationqueue

我正在开发一个应用程序,当它开始执行时,它必须从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];
}

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

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

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

[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];

所以,不要逐个调用这个方法。我想我可以使用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];
        });
    });

}
在我将所有内容保存到[self nextController]之前调用

Core Data方法并且我有错误..

2 个答案:

答案 0 :(得分:5)

由于你的所有四种方法

[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];

异步,应该清楚为什么语句

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

看来,你想要同时启动所有四个异步方法

时,你必须调用语句[self nextController]取决于是否有任何依赖来调用上述四种异步方法的最终结果

例如,您可以声明:

一个。 [self nextController]成功完成后,loadAppInfo:将被执行。所有其他异步方法都无关紧要。

解决方案如下所示:

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

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

B中。当所有四个异步方法成功完成(或多于一个,并且所有其他方法都无关紧要)时,将执行[self nextController]

有一些方法可以解决这个问题。一种方法是使用调度组,信号量和一些状态变量和调度队列来确保并发。但是,这是非常详细的,最终会导致阻塞一个线程,无法取消,并且也非常不理想(除此之外它看起来也是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

B中。当所有四个操作成功完成时,NextControllerOperation将被启动:

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

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

看起来不错?否?


更具吸引力的方法:承诺

如果这个NSOperations的解决方案看起来不太好,那就太精细了(五个NSOperation子类!)或者其他什么,这是一个更有吸引力的方法,它使用第三方库实现承诺

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

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

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

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

- (RXPromise*) doSomethingAsync;

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

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

NSArray* allTasks = @[
    [self loadAppInfo],
    [self loadCountriesFromJson],
    [self loadCategoriesFromWS],
    [self loadSplashFromWS]    
]; 
... 

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

现在,我们继续并定义当此数组中的所有任务成功完成或任何任务失败时将发生的情况。这里我们使用辅助类方法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;
});

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

解释如下:


说明:

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

还有一些其他实现(SHXPromiseOMPromises以及更多),只需稍加努力就可以将下面的代码移植到其他promise库中。

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

- (RXPromise*) loadAppInfo;
- (RXPromise*) loadCountriesFromJson;
- (RXPromise*) loadCategoriesFromWS;
- (RXPromise*) loadSplashFromWS;

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

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

异步任务将创建承诺,并且最终必须解决"它通过fulfillWithValue:获得最终结果,或者失败时通过rejectWithReason:发生错误。请参阅下文,如何创建RXPromise,立即从异步方法返回,并且"已解决"稍后当任务完成或失败时。

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

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

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

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

promise.then(<success_handler>, <error_handler>);

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

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

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

promise.thenOn(dispatch_queue, <success_handler>, <error_handler>);

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

可以执行两个或多个异步任务随后(又名 chained ),其中每个任务都会生成一个结果,该结果将成为后续任务的输入。

&#34; chaining&#34;的简短形式两个异步方法看起来像这样:

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

此处,asyncBWithResult:只会在asyncA成功完成后执行。上面的表达式返回一个Promise finalResult ,它表示asyncBWithResult:&#34;返回&#34;的最终结果。当它结束时的结果,或者它包含来自链中失败的任何任务的错误。

回到你的问题:

您的方法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;
}

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

重构了这两个方法,它们只执行你想要完成的任务,并且还重构了其他异步方法,现在它们像上面重构的方法一样返回一个promise,你现在可以完成你的任务,如开头所示。 / p>

答案 1 :(得分:0)

  

GCD在调用加载映像时管理它,直到完成所有操作,然后调用下一个ViewController。我相信这是一个很好的解决方案吗?

一般的经验法则是在最高抽象级别上运作。

在这种情况下,它意味着使用NSOperation子类。您可以创建一个专用队列,并以这样的方式安排您的操作,即只有在所有操作完成后才会关闭加载图像,例如,由

NSOperation *goForward = [MyGoForwardOperation new]; // you define this subclass
NSOperation *loadSomething = [MyLoadSomethingOperation new];
NSOperation *loadAnother = [MyLoadAnotherThingOperation new];
[goForward addDependency: loadOperation];
[goForward addDependency: loadAnother];

NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation: loadSomething];
[queue addOperation: loadAnother];

[[NSOperationQueue mainQueue] addOperation: goForward];

请注意,在此示例中,goForward将在主线程上运行,但在后台操作完成后

您需要仔细对MyLoadSomethingOperation进行编程才能使其工作,请仔细阅读子类NSOperation或子类AFHTTPRequestOperation,因为无论如何都要使用它。

  

[self nextController]方法在我拥有所有内容之前被调用

是的,您应该在后台线程上搜索保存到Core Data;这本身就是一个很大的话题。