ios - 使用 NSCondition 等待异步方法

标签 ios multithreading nsurlrequest nscondition

我正在通过互联网异步下载四个 plist 文件。我需要等到所有四个文件都下载完毕,直到我在第一次运行时推送 UIViewController,或者在所有后续运行中刷新数据并重新加载所有 UITableView。

第一次运行时,一切都很完美。不过,刷新时,所有四个 url 请求都会被调用并启动,但永远不会调用它们的完成或失败 block ,并且 UI 会卡住。这很奇怪,因为我在后台线程中执行所有操作。我一直无法弄清楚为什么会发生这种情况。

第一个加载和刷新方法以相同的方式调用四个“更新”方法,并以相同的方式使用 NSCondition。

第一次运行:

- (void)loadContentForProgram:(NSString *)programPath
{
    NSLog(@"Start Load Program");
    AppDelegate *myDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    hud = [[MBProgressHUD alloc] initWithView:myDelegate.window];
    [myDelegate.window addSubview:hud];
    hud.labelText = @"Loading...";
    hud.detailsLabelText = @"Loading Data";
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        //Do stuff here to load data from files

        //Update From online files
        hud.detailsLabelText = @"Updating Live Data";
        resultLock = NO;
        progressLock = NO;
        recallLock = NO;
        stageLock = NO;

        condition = [[NSCondition alloc] init];
        [condition lock];

        [self updateCurrentCompsText];
        [self updateCompetitionResults];
        [self updateCompetitionRecalls];
        [self updateCompetitionProgress];


        while (!resultLock) {
            [condition wait];
        }
        NSLog(@"Unlock");
        while (!stageLock) {
            [condition wait];
        }
        NSLog(@"Unlock");
        while (!recallLock) {
            [condition wait];
        }
        NSLog(@"Unlock");
        while (!progressLock) {
            [condition wait];
        }
        NSLog(@"Unlock");
        [condition unlock];
        updateInProgress = NO;
        //Reset Refresh controls and table views
        self.refreshControlsArray = [[NSMutableArray alloc] init];
        self.tableViewsArray = [[NSMutableArray alloc] init];
        NSLog(@"Finished Loading Program");
        [[NSNotificationCenter defaultCenter] postNotificationName:@"WMSOFinishedLoadingProgramData" object:nil]; //Pushes view controller
        dispatch_async(dispatch_get_main_queue(), ^{
            [MBProgressHUD hideHUDForView:myDelegate.window animated:YES];
        });
    });
}

刷新数据时:

- (void)updateProgramContent
{
    if (!updateInProgress) {
        updateInProgress = YES;
        for (int i = 0; i < self.refreshControlsArray.count; i++) {
            if (!((UIRefreshControl *)self.refreshControlsArray[i]).refreshing) {
                [self.refreshControlsArray[i] beginRefreshing];
                [self.tableViewsArray[i] setContentOffset:CGPointMake(0.0, 0.0) animated:YES];
            }
        }

        resultLock = NO;
        stageLock = NO;
        recallLock = NO;
        progressLock = NO;
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

            condition = [[NSCondition alloc] init];
            [condition lock];

            [self updateCompetitionProgress];
            [self updateCompetitionRecalls];
            [self updateCompetitionResults];
            [self updateCurrentCompsText];

            while (!resultLock) {
                [condition wait];
            }
            NSLog(@"Unlock");
            while (!stageLock) {
                [condition wait];
            }
            NSLog(@"Unlock");
            while (!recallLock) {
                [condition wait];
            }
            NSLog(@"Unlock");
            while (!progressLock) {
                [condition wait];
            }
            NSLog(@"Unlock");
            [condition unlock];
        });

        for (int i = 0; i < self.refreshControlsArray.count; i++) {
            [self.refreshControlsArray[i] performSelector:@selector(endRefreshing) withObject:nil afterDelay:1.0];
            [self.tableViewsArray[i] performSelector:@selector(reloadData) withObject:nil afterDelay:1.0];
        }
        updateInProgress = NO;
    }
}

上面每个加载方法中出现的下面的 block 对应于下载和更新特定数据的方法。

[self updateCompetitionProgress];
[self updateCompetitionRecalls];
[self updateCompetitionResults];
[self updateCurrentCompsText];

运行:

- (void)updateCompetitionResults
{
    __block NSDictionary *competitionResultsData = nil;
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"Some URL",[self.programName stringByReplacingOccurrencesOfString:@" " withString:@"%20"]]] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:20.0];
    AFPropertyListRequestOperation *operation = [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {
        competitionResultsData = (NSDictionary *)propertyList;
        [competitionResultsData writeToFile:[@"SOME LOCAL PATH"] atomically:NO];
        [self updateCompetitionResultsWithDictionary:competitionResultsData];
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {
        competitionResultsData = [NSDictionary dictionaryWithContentsOfFile:[@"SOME LOCAL PATH"]];
        NSLog(@"Failed to retreive competition results: %@", error);
        [self updateCompetitionResultsWithDictionary:competitionResultsData];
    }];
    [operation start];
}

并且完成和失败 block 调用相同的方法来更新数据

- (void)updateCompetitionResultsWithDictionary:(NSDictionary *)competitionResultsData
{
    //Do Stuff with the data here
    resultLock = YES;
    [condition signal];
}

那么,为什么这在第一次运行时有效,但在后续运行中不起作用?

最佳答案

正如我在上面的评论中提到的,最明显的问题是您在初始化 condition 之前调用使用 condition 的方法。确保在开始调用 condition 等之前初始化 updateCompetitionResults 等。


就更彻底的改变而言,我可能建议完全淘汰 NSCondition,并使用操作队列:

  1. 我可能会使用 NSOperationQueue (或者如果您愿意的话,您也可以使用调度组,但我喜欢操作队列能够配置您可以操作的并发操作数......如果您达到某个点如果你想取消操作,我认为 NSOperationQueue 也提供了一些不错的功能)。然后,您可以将每个下载和处理定义为单独的 NSOperation (每个下载应该同步发生,因为它们在操作队列中运行,您可以获得异步操作的好处,但您可以在下载后立即启动后处理下载完成)。然后,您只需将它们排队以异步运行,但定义一个最终操作,该操作依赖于其他四个操作,一旦四个下载完成,就会立即开始。 (顺便说一句,我使用 NSBlockOperation ,它为 NSOperation 对象提供 block 功能,但您可以按照自己的方式进行操作。)

  2. 尽管您的 updateProgramContent 可能会异步下载,但它会依次处理四个下载的文件。因此,如果第一次下载需要一段时间才能下载,则会阻碍其他下载的后处理。相反,我喜欢将四个 plist 文件中每个文件的下载和后处理封装在一个 NSOperation 中。因此,我们不仅可以享受下载的最大并发性,还可以享受后处理的最大并发性。

  3. 我可能倾向于使用 AFNetworkingNSDictionary 功能,而不是使用 NSArray(我通常非常喜欢)plist 相关方法,这些功能允许您从网络下载 plist 并加载它们到适当的结构中。这些 dictionaryWithContentsOfURLarrayWithContentsOfURL 同步运行,但因为我们是在后台操作中执行此操作,所以一切都按照您想要的方式异步运行。这也绕过了将它们保存到文件的过程。如果您希望将它们保存到 Documents 目录中的文件中,您也可以轻松做到这一点。显然,如果您在下载 plist 文件时执行一些复杂的操作(例如,您的服务器正在进行某些质询-响应身份验证),则无法使用方便的 NSDictionaryNSArray 方法。但如果您不需要所有这些,简单的 NSDictionaryNSArray 方法、___WithContentsOfURL 会让生活变得非常简单。

把所有这些放在一起,它可能看起来像:

@interface ViewController ()

@property (nonatomic, strong) NSArray *competitions;
@property (nonatomic, strong) NSDictionary *competitionResults;
@property (nonatomic, strong) NSDictionary *competitionRecalls;
@property (nonatomic, strong) NSDictionary *competitionProgress;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self transfer];
}

- (void)allTransfersComplete
{
    BOOL success;

    if (self.competitions == nil)
    {
        success = FALSE;
        NSLog(@"Unable to download competitions");
    }

    if (self.competitionResults == nil)
    {
        success = FALSE;
        NSLog(@"Unable to download results");
    }

    if (self.competitionRecalls == nil)
    {
        success = FALSE;
        NSLog(@"Unable to download recalls");
    }

    if (self.competitionProgress == nil)
    {
        success = FALSE;
        NSLog(@"Unable to download progress");
    }

    if (success)
    {
        NSLog(@"all done successfully");
    }
    else
    {
        NSLog(@"one or more failed");
    }
}

- (void)transfer
{
    NSURL *baseUrl = [NSURL URLWithString:@"http://insert.your.base.url.here/competitions"];
    NSURL *competitionsUrl = [baseUrl URLByAppendingPathComponent:@"competitions.plist"];
    NSURL *competitionResultsUrl = [baseUrl URLByAppendingPathComponent:@"competitionresults.plist"];
    NSURL *competitionRecallsUrl = [baseUrl URLByAppendingPathComponent:@"competitionrecalls.plist"];
    NSURL *competitionProgressUrl = [baseUrl URLByAppendingPathComponent:@"competitionprogress.plist"];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 4; // if your server doesn't like four concurrent requests, you can ratchet this back to whatever you want

    // create operation that will be called when we're all done

    NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{

        // any stuff that can be done in background should be done here

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            // any user interface stuff should be done here; I've just put this in a separate method so this method doesn't get too unwieldy

            [self allTransfersComplete];
        }];
    }];

    // a variable that we'll use as we create our four download/process operations

    NSBlockOperation *operation;

    // create competitions operation

    operation = [NSBlockOperation blockOperationWithBlock:^{

        // download the competitions and load it into the ivar
        //
        // note, if you *really* want to download this to a file, you can 
        // do that when the download is done

        self.competitions = [NSArray arrayWithContentsOfURL:competitionsUrl];

        // if you wanted to do any post-processing of the download
        // you could do it here.            
        NSLog(@"competitions = %@", self.competitions);
    }];
    [completionOperation addDependency:operation];

    // create results operation

    operation = [NSBlockOperation blockOperationWithBlock:^{

        self.competitionResults = [NSDictionary dictionaryWithContentsOfURL:competitionResultsUrl];

        NSLog(@"competitionResults = %@", self.competitionResults);
    }];
    [completionOperation addDependency:operation];

    // create recalls operation

    operation = [NSBlockOperation blockOperationWithBlock:^{

        self.competitionRecalls = [NSDictionary dictionaryWithContentsOfURL:competitionRecallsUrl];

        NSLog(@"competitionRecalls = %@", self.competitionRecalls);
    }];
    [completionOperation addDependency:operation];

    // create progress operation

    operation = [NSBlockOperation blockOperationWithBlock:^{

        self.competitionProgress = [NSDictionary dictionaryWithContentsOfURL:competitionProgressUrl];

        NSLog(@"competitionProgress = %@", self.competitionProgress);
    }];
    [completionOperation addDependency:operation];

    // queue the completion operation (which is dependent upon the other four)

    [queue addOperation:completionOperation];

    // now queue the four download and processing operations

    [queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
}

@end

现在,我不知道你的 plist 中哪些是数组,哪些是字典(在我的示例中,我将竞赛作为一个数组,其余的都是由竞赛 id 键控的字典),但希望你能了解什么我正在射击。最大化并发,消除 NSCondition 逻辑,真正充分利用 NSOperationQueue 等。

这可能太多了,但我只是将它作为 NSCondition 的替代品。如果您当前的技术有效,那就太好了。但上面概述了我将如何应对这样的挑战。

关于ios - 使用 NSCondition 等待异步方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14013947/

相关文章:

ios - 使UIToolBar和UISearchBar具有黑色背景

ios - iOS获取键盘语言

php - 如何将 NSString 从 JSON 转换为 NSDictionary?

java - 多线程环境下迭代集合: ConcurrentModificationException

ios - curl -u 相当于 NSURLRequest in swift

ios - 如何锁定 UIAlertController 方向?

java - Apache Thrift 在不同线程中重用连接

c - 如何在运行时更改宏定义

iphone - 我的 iOS 应用程序是否使用加密?

ios - 你能在不破坏后退按钮的情况下拦截 UIWebView 中的 NSURLRequests 吗?