Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices for making a queue of NSURLSessionTasks

What are the best practices for making a serial queue of NSURLSessionTasks ?

In my case, I need to:

  1. Fetch a URL inside a JSON file (NSURLSessionDataTask)
  2. Download the file at that URL (NSURLSessionDownloadTask)

Here’s what I have so far:

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

           }
 ];

Apart from the fact that writing a completion block within another completion looks messy, I am getting an EXC_BAD_ACCESS error when I call [fileDownloadTask resume]... Even though fileDownloadTask is not nil!

So, what is the best of way of sequencing NSURLSessionTasks?

like image 875
Eric Avatar asked Dec 16 '13 18:12

Eric


3 Answers

You need to use this approach which is the most straight forward: https://stackoverflow.com/a/31386206/2308258

Or use an operation queue and make the tasks dependent on each others

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

Regarding the HTTPMaximumConnectionsPerHost method

An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1

HTTPMaximumConnectionsPerHost only ensure that one shared connection will be used for the tasks of that session but it does not mean that they will be processed serially.

You can verify that on the network level using http://www.charlesproxy.com/, you wil discover that when setting HTTPMaximumConnectionsPerHost, your tasks will be still be started together at the same time by NSURLSession and not serially as believed.

Expriment 1:

  • Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
  • With task1: url = download.thinkbroadband.com/20MB.zip
  • With task2: url = download.thinkbroadband.com/20MB.zip

    1. calling [task1 resume];
    2. calling [task2 resume];

Result: task1 completionBlock is called then task2 completionBlock is called

The completion blocks might be called in the order you expected in case the tasks take the same amount of time however if you try to download two different thing using the same NSURLSession you will discover that NSURLSession does not have any underlying ordering of your calls but only completes whatever finishes first.

Expriment 2:

  • Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
  • task1: url = download.thinkbroadband.com/20MB.zip
  • task2: url = download.thinkbroadband.com/10MB.zip (smaller file)

    1. calling [task1 resume];
    2. calling [task2 resume];

Result: task2 completionBlock is called then task1 completionBlock is called

In conclusion you need to do the ordering yourself, NSURLSession does not have any logic about ordering requests it will just call the completionBlock of whatever finishes first even when setting the maximum number of connections per host to 1

PS: Sorry for the format of the post I do not have enough reputation to post screenshots.

like image 66
matanwrites Avatar answered Nov 05 '22 23:11

matanwrites


Edit:

As mataejoon has pointed out, setting HTTPMaximumConnectionsPerHost to 1 will not guarantee that the connections are processed serially. Try a different approach (as in my original answer bellow) if you need a reliable serial queue of NSURLSessionTask.


An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 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; } 

then add tasks to it in the order you want.

NSURLSessionDataTask *sizeTask =  [[[self class] session] dataTaskWithURL:url                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
like image 38
Eric Avatar answered Nov 05 '22 22:11

Eric


#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

and use like this

    __block __weak NSURLSessionTask * wsessionTask;
    use_wself();

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

        [self.sessionTaskQueue sessionTaskFinished:wsessionTask];

        ...

    }];

    [self.sessionTaskQueue addSessionTask:wsessionTask];
like image 31
Peter Lapisu Avatar answered Nov 05 '22 23:11

Peter Lapisu