制作NSURLSessionTask队列的最佳实践

时间:2013-12-16 18:59:21

标签: ios cocoa-touch nsurlsession

制作NSURLSessionTasks的串行队列的最佳做法是什么?

就我而言,我需要:

  1. 获取JSON文件(NSURLSessionDataTask
  2. 中的URL
  3. 下载该网址(NSURLSessionDownloadTask
  4. 的文件

    这是我到目前为止所拥有的:

    session = [NSURLSession sharedSession];
    
    //Download the JSON:
    NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];
    
    NSURLSessionDataTask *task =
    [session dataTaskWithRequest:dataRequest
               completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    
                   //Figure out the URL of the file I want to download:
                   NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
                   NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:@"download_url"]];
    
                   NSURLSessionDownloadTask *fileDownloadTask =
                   [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
                                  completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
                                      NSLog(@"completed!");
                                  }];
    
                   [fileDownloadTask resume];
    
               }
     ];
    

    除了在另一个完成中写一个完成块看起来很乱的事实,我在调用[fileDownloadTask resume]时收到EXC_BAD_ACCESS错误...即使fileDownloadTask不是零!

    那么,排序NSURLSessionTasks的最佳方式是什么?

4 个答案:

答案 0 :(得分:18)

您需要使用最直接的方法:https://stackoverflow.com/a/31386206/2308258

或者使用操作队列并使任务相互依赖

=============================================== ========================

关于HTTPMaximumConnectionsPerHost方法

  

实现NSURLSessionTasks的先进先出串行队列的简单方法是在NSURLSession上运行其HTTPMaximumConnectionsPerHost属性设置为1的所有任务

HTTPMaximumConnectionsPerHost仅确保一个共享连接将用于该会话的任务,但这并不意味着它们将被串行处理。

您可以使用http://www.charlesproxy.com/在网络级别验证,您会发现在设置HTTPMaximumConnectionsPerHost时,您的任务仍将由NSURLSession同时启动,而不是按照相同的顺序启动。

实验1:

  • 使用HTTPMaximumConnectionsPerHost将NSURLSession声明为1
  • 使用task1:url = download.thinkbroadband.com/20MB.zip
  • 使用task2:url = download.thinkbroadband.com/20MB.zip

    1. 调用[task1 resume];
    2. 调用[task2 resume];

结果:调用task1 completionBlock然后调用task2 completionBlock

如果您的任务花费相同的时间,则可能会按预期的顺序调用完成块如果您尝试使用相同的NSURLSession下载两个不同的东西,您会发现NSURLSession会执行没有你的电话的任何潜在订购,但只完成任何完成。

实验2:

  • 使用HTTPMaximumConnectionsPerHost将NSURLSession声明为1
  • task1:url = download.thinkbroadband.com/20MB.zip
  • task2:url = download.thinkbroadband.com/10MB.zip(较小的文件

    1. 调用[task1 resume];
    2. 调用[task2 resume];

结果:调用task2 completionBlock然后调用task1 completionBlock

总之,您需要自己进行排序,NSURLSession没有任何关于排序请求的逻辑,即使将每个主机的最大连接数设置为1

PS:很抱歉该帖子的格式我没有足够的声誉来发布截图。

答案 1 :(得分:13)

修改

正如mataejoon指出的那样,将HTTPMaximumConnectionsPerHost设置为1将保证连接是按顺序处理的。 如果您需要NSURLSessionTask的可靠序列队列,请尝试不同的方法(如我原来的答案)。


实现NSURLSessionTasks的先进先出串行队列的简单方法是在NSURLSession属性设置为1的{​​{1}}上运行所有任务}:

+ (NSURLSession *)session
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

        [configuration setHTTPMaximumConnectionsPerHost:1];

        session = [NSURLSession sessionWithConfiguration:configuration];

    });
    return session;
}

然后按照您想要的顺序向添加任务

NSURLSessionDataTask *sizeTask = 
[[[self class] session] dataTaskWithURL:url 
                          completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

答案 2 :(得分:1)

#import "SessionTaskQueue.h"

@interface SessionTaskQueue ()

@property (nonatomic, strong) NSMutableArray * sessionTasks;
@property (nonatomic, strong) NSURLSessionTask * currentTask;

@end

@implementation SessionTaskQueue

- (instancetype)init {

    self = [super init];
    if (self) {

        self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];

    }
    return self;

}

- (void)addSessionTask:(NSURLSessionTask *)sessionTask {

    [self.sessionTasks addObject:sessionTask];
    [self resume];

}

// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {

    self.currentTask = nil;
    [self resume];

}

- (void)resume {

    if (self.currentTask) {
        return;
    }

    self.currentTask = [self.sessionTasks firstObject];
    if (self.currentTask) {
        [self.sessionTasks removeObjectAtIndex:0];
        [self.currentTask resume];
    }

}

@end

并像这样使用

    __block __weak NSURLSessionTask * wsessionTask;
    use_wself();

    wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
        use_sself();

        [self.sessionTaskQueue sessionTaskFinished:wsessionTask];

        ...

    }];

    [self.sessionTaskQueue addSessionTask:wsessionTask];

答案 3 :(得分:0)

我使用NSOperationQueue(正如Owen所建议的那样)。将NSURLSessionTasks放在NSOperation子类中并设置任何依赖项。相关任务将等到它们所依赖的任务在运行之前完成,但不会检查状态(成功或失败),因此添加一些逻辑来控制该过程。

在我的情况下,第一个任务检查用户是否拥有有效帐户并在必要时创建一个帐户。在第一个任务中,我更新NSUserDefault值以指示帐户有效(或存在错误)。第二个任务检查NSUserDefault值,如果所有OK都使用用户凭据将一些数据发布到服务器。

(在单独的NSOperation子类中粘贴NSURLSessionTasks也使我的代码更容易导航)

将NSOperation子类添加到NSOperationQueue并设置任何依赖项:

 NSOperationQueue  *ftfQueue = [NSOperationQueue new];
 FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
 [createFTFAccount setUserid:@"********"];  // Userid to be checked / created
 [ftfQueue addOperation:createFTFAccount];
 FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
 [postFTFRoute addDependency:createFTFAccount];
 [ftfQueue addOperation:postFTFRoute];

在第一个NSOperation子类中检查服务器上是否存在帐户:

@implementation FTFCreateAccount
{
    NSString *_accountCreationStatus;
}



- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;

    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setValue:@"CHECKING" forKey:ftfAccountStatusKey];

    // Setup and Run the NSURLSessionTask
    [self createFTFAccount:[self userid]];

    // Hold it here until the SessionTask completion handler updates the _accountCreationStatus
    // Or the process takes too long (possible connection error)

    while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
        NSDate *currentDate = [[NSDate alloc] init];     
        timeElapsed = [currentDate timeIntervalSinceDate:startDate];    
    }
    if ([_accountCreationStatus isEqualToString:@"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];

    if ([self isCancelled]) {
        NSLog(@"DEBUG FTFCreateAccount Cancelled" );
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        [userDefaults setValue:@"ERROR" forKey:ftfAccountStatusKey];            
    }    
}

在下一个NSOperation发布数据中:

@implementation FTFPostRoute
{
    NSString *_routePostStatus;
}

- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;
    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];

    if ([ftfAccountStatus isEqualToString:@"ERROR"])
    {
        // There was a ERROR in creating / accessing the user account.  Cancel the post
        [self cancel];
    } else
    {
        // Call method to setup and run json post

        // Hold it here until a reply comes back from the operation
        while ((!_routePostStatus) && (timeElapsed < 3)) {
            NSDate *currentDate = [[NSDate alloc] init];          
            timeElapsed = [currentDate timeIntervalSinceDate:startDate];                
            NSLog(@"FTFPostRoute time elapsed: %f", timeElapsed);                
        }

    }


    if ([self isCancelled]) {
        NSLog(@"FTFPostRoute operation cancelled");
    }
}