【问题标题】:Best way to manage many block calls管理许多块调用的最佳方法
【发布时间】:2013-11-20 14:07:51
【问题描述】:

我正在开发一个应用程序,当它开始执行时,它必须从 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 之前被调用并且我有错误..

【问题讨论】:

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


    【解决方案1】:

    因为你所有的四种方法

    [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]取决于对依赖 >以上四种异步方法的最终结果。

    例如,您可以声明:

    A. [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];
    

    好看吗?没有?


    更吸引人的方法:Promises

    如果这个带有NSOperations 的解决方案看起来不太好,过于复杂(五个NSOperation 子类!)或其他什么,这里有一个更吸引人的方法,它使用实现Promises 的第三方库时间>。

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

    披露:这里的示例代码使用了第三方库RXPromise,它根据Promise/A+ specification 实现了一个Promise。我是RXPromise 库的作者。

    还有一些在 Objective-C 中实现的 Promise 库,但你还是可以看看 RXPromise ;)(请参阅下面的链接)

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

    - (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 包含任务已启动并且现在所有并发运行的 Promise。

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

    请参阅上面代码中的 cmets,了解如何使用“模糊”then 定义成功和错误处理程序(在所有任务完成后调用)。

    解释如下:


    说明:

    下面的代码使用了 RXPromise 库。您可以在 GitHub 上获取RXPromise Library 的源代码。

    还有一些其他实现(SHXPromiseOMPromises 等等),只要稍加努力,也应该可以将下面的代码移植到其他 Promise 库中。

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

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

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

    我已经重构了你原来的方法,以便更好地利用 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;
    }
    

    关于 Promise 的一些进一步说明:

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

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

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

    注意:使用 RXPromise,您可以根据需要注册处理程序块 whenwhere,以及 任意数量! RXPromise 是完全线程安全的。你只需要在某处或需要的时候保持对 Promise 的强烈引用。不过,您需要保留引用,即使您设置了处理程序。

    处理程序块将在私有队列上执行。这意味着,您不知道处理程序将在其中执行的执行上下文(也称为线程),除非您使用此变体:

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

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

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

    两个异步方法的“链接”的简短形式如下所示:

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

    这里,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;
    }
    

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

    重构了这两个方法,它们只执行你打算完成的事情,并且重构了其他异步方法,它们现在返回一个承诺,就像上面重构的方法一样,你现在可以完成你的任务,如开始所示。

    【讨论】:

      【解决方案2】:

      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] 方法在我拥有一切之前被调用

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

      【讨论】:

        猜你喜欢
        • 2011-03-23
        • 2019-12-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多