56

I'm trying to build a bulk image downloader, where images can be added to a queue on the fly to be downloaded, and I can find out the progress and when they're done downloading.

Through my reading it seems like NSOperationQueue for the queue functionality and NSURLSession for the network functionality seems like my best bet, but I'm confused as to how to use the two in tandem.

I know I add instances of NSOperation to the NSOperationQueue and they get queued. And it seems I create a download task with NSURLSessionDownloadTask, and multiple if I need multiple tasks, but I'm not sure how I put the two together.

NSURLSessionDownloadTaskDelegate seems to have all the information I need for download progress and completion notifications, but I also need to be able to stop a specific download, stop all the downloads, and deal with the data I get back from the download.

Doug Smith
  • 29,668
  • 57
  • 204
  • 388
  • 1
    I would have a look at https://github.com/AFNetworking/AFNetworking – nrj Feb 20 '14 at 21:07
  • Or https://github.com/rs/SDWebImage – Reid Main Feb 20 '14 at 21:52
  • 27
    I want to do this myself without the use of a library. – Doug Smith Feb 20 '14 at 22:22
  • @nick I'd agree with you in general, but AFNetworking's `NSURLSession` implementation does not yet use `NSOperationQueue`. Mattt [reports that he was working on a branch that does that](https://github.com/AFNetworking/AFNetworking/issues/1504#issuecomment-33736042), though it's been long enough that I'm wondering if he's run into some challenges. – Rob May 23 '14 at 19:33

7 Answers7

52

Your intuition here is correct. If issuing many requests, having an NSOperationQueue with maxConcurrentOperationCount of 4 or 5 can be very useful. In the absence of that, if you issue many requests (say, 50 large images), you can suffer timeout problems when working on a slow network connection (e.g. some cellular connections). Operation queues have other advantages, too (e.g. dependencies, assigning priorities, etc.), but controlling the degree of concurrency is the key benefit, IMHO.

If you are using completionHandler based requests, implementing operation-based solution is pretty trivial (it's the typical concurrent NSOperation subclass implementation; see the Configuring Operations for Concurrent Execution section of the Operation Queues chapter of the Concurrency Programming Guide for more information).

If you are using the delegate based implementation, things start to get pretty hairy pretty quickly, though. This is because of an understandable (but incredibly annoying) feature of NSURLSession whereby the task-level delegates are implemented at the session-level. (Think about that: Two different requests that require different handling are calling the same delegate method on the shared session object. Egad!)

Wrapping a delegate-based NSURLSessionTask in an operation can be done (I, and others I'm sure, have done it), but it involves an unwieldy process of having the session object maintain a dictionary cross referencing task identifiers with task operation objects, have it pass these task delegate methods passed to the task object, and then have the task objects conform to the various NSURLSessionTask delegate protocols. It's a pretty significant amount of work required because NSURLSession doesn't provide a maxConcurrentOperationCount-style feature on the session (to say nothing of other NSOperationQueue goodness, like dependencies, completion blocks, etc.).

And it's worth pointing out that operation-based implementation is a bit of a non-starter with background sessions, though. Your upload/download tasks will continue to operate well after the app has been terminated (which is a good thing, that's fairly essential behavior in a background request), but when your app is restarted, the operation queue and all of its operations are gone. So you have to use a pure delegate-based NSURLSession implementation for background sessions.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • @Rob- i also have the same confusion regarding this. Can you please explain. This is my question:http://stackoverflow.com/questions/24114093/replacing-nsurlconnection-with-nsurlsession. You seem to have a better understanding of this. I want to develop a generic library/framework for NetworkCommunication. Dont want to use AFNetworking. So, i can learn more. – user694688 Jun 09 '14 at 07:12
  • @Rob how much of a overhead will it be if we create new NSURLSession in the operation rather than sharing same operation. If we do that then we dont have to worry about session level delegate being called on different threads? – Ezio Jun 02 '15 at 19:43
  • @Sam There was a thread in which someone was complaining about `NSURLSession` memory consumption (http://stackoverflow.com/q/30106960/1271826). I was unable to reproduce his problem and we later concluded that his memory problem stemmed from instantiating a new `NSURLSession` for each request, something I didn't experience when reused existing session objects. I'd suggest stress testing this behavior and not assume one can safely instantiate new `NSURLSession` for every request. – Rob Jun 02 '15 at 20:03
  • 1
    @Rob Thanks for the quick response. I did do some testing and did not see any memory issues. Probably have to do some more testing. – Ezio Jun 02 '15 at 20:47
  • Another possible issue with a new session per request: we found starting a new session for each request was an easy way to swamp a server with requests, as the max connections per host are session object specific. Sharing a session object allows you to fire many requests at a single host and the session handles serialising them as needed. – Mike Rhodes Aug 28 '15 at 10:54
  • 1
    @Rob This is a great answer and it makes me wonder: is there any reason at all to use delegate-based requests instead of completionHandler-based requests, if the completionHandler-based requests are so much easier to wrap? The only benefit I can see is that it lets you access the incompletely downloaded data before cancellation, but as this data will be invalid, and NSOperation does not allow you to suspend/resume the download, that seems pointless. Is there some benefit I'm missing? – algal Apr 20 '16 at 18:40
  • 5
    @algal There are many features that aren't available without delegate. For example, progress updates for large downloads so you can update a progress view. Or handling authentication challenges. Or identifying redirects. Or, if you're doing some streaming protocol. So, there are definitely times you want delegate. But for those cases where you don't, life is much easier. – Rob Apr 20 '16 at 19:55
  • It does seem remarkable that one can't simply put `URLSession` tasks inside an ordinary serial queue - perhaps I misunderstand something Eg http://stackoverflow.com/questions/42056365/how-to-sync-serial-queue-for-urlsession-tasks – Fattie Feb 06 '17 at 15:45
42

Conceptually, NSURLSession is an operation queue. If you resume an NSURLSession task and breakpoint on the completion handler, the stack trace can be quite revealing.

Here's an excerpt from the ever faithful Ray Wenderlich's tutorial on NSURLSession with an added NSLog statement to breakpoint on executing the completion handler:

NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
          completionHandler:^(NSData *data,
                              NSURLResponse *response,
                              NSError *error) {
            // handle response
            NSLog(@"Handle response"); // <-- breakpoint here       

  }] resume];

NSOperationQueue Serial Queue breakpoint

Above, we can see the completion handler being executed in Thread 5 Queue: NSOperationQueue Serial Queue.

So, my guess is that each NSURLSession maintains it's own operation queue, and each task added to a session is - under the hood - executed as an NSOperation. Therefore, it doesn't make sense to maintain an operation queue that controls NSURLSession objects or NSURLSession tasks.

NSURLSessionTask itself already offers equivalent methods such as cancel, resume, suspend, and so on.

It's true that there is less control than you would have with your own NSOperationQueue. But then again, NSURLSession is a new class the purpose of which is undoubtably to relieve you of that burden.

Bottom line: if you want less hassle - but less control - and trust Apple to perform the network tasks competently on your behalf, use NSURLSession. Otherwise, roll your own with NSURLConnection and your own operation queues.

Max MacLeod
  • 26,115
  • 13
  • 104
  • 132
  • Great answer, everyone who wrapped NSURLSession in an operation queue is going to explode when they find this out! You could perhaps add that all the tasks in the session (or queue!) can be cancelled with [session invalidateAndCancel]. Also I find it interesting if you pass nil delegateQueue to sessionWithConfiguration, the tasks operate sequentially, but if you pass a new queue they operate simultaneously. My guess would be sequential (or serial) is the behaviour most people are after when they start trying to wrap in a queue. – malhal Sep 03 '16 at 23:31
  • 3
    A big benefit of using an NSOperationQueue is you can add dependencies between operations. IE run operation 1 before operation 2. You cannot do that with NSURLSession since the session does not expose that queue. – lostintranslation Feb 17 '17 at 14:42
  • I know this question is old, but how is benefit adding dependencies when BlockOperation finishes before the request? URLSession make a async request, while the request is ocurring, the rest of the block keep executing. Based on that if you have OP1 and OP2, OP2 having depency on OP1, it could be executed even when request on OP1 hasn't finished. – Vertig0 Nov 19 '17 at 04:45
  • @max-macleod If you trigger a lot of tasks at the same time and the single tasks take some time, the latter ones will suffer from timeouts. So wrapping in a queue is essential here. – mAu Nov 24 '17 at 11:38
  • 1
    Disagree, we should not rely on the non-public implementation detail as this could change at any point without notice. Apple doesn't publicly expose the operation queue so we should not assume how something works. See this WWDC session on `NSOperation` where `URLSessionTasks` are wrapped in `Operation` sub-classes. https://developer.apple.com/videos/play/wwdc2015/226/ – Camsoft Oct 15 '18 at 11:03
  • @Camsoft I'm not saying we should rely on a non-public implementation. Only that by debugging we discover that - under the hood - NSURLSession is an NSOperationQueue. It can be useful to keep this in mind in designing one's implementations. – Max MacLeod Oct 19 '18 at 14:15
  • I disagree, this could change at any anytime why should it influence the design of your implementation? – Camsoft Oct 31 '18 at 12:35
7

Update: The executing and finishing properties hold the knowledge about the status of the current NSOperation. Once finishing is set to YES and executing to NO, your operation is considered as finished. The correct way of dealing with it does not require a dispatch_group and can simply be written as an asynchronous NSOperation:

  - (BOOL) isAsynchronous {
     return YES;
  }

  - (void) main
    {
       // We are starting everything
       self.executing = YES;
       self.finished = NO;
    
       NSURLSession * session = [NSURLSession sharedInstance];
    
       NSURL *url = [NSURL URLWithString:@"http://someurl"];
    
       NSURLSessionDataTask * dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
    
          /* Do your stuff here */
    
         NSLog("Will show in second");
    
         self.executing = NO;
         self.finished = YES;
       }];
    
       [dataTask resume]
   }

The term asynchronous is quite misleading and does not refers to the differentiation between UI (main) thread and background thread.

If isAsynchronous is set to YES, it means that some part of the code is executed asynchronously regarding the main method. Said differently: an asynchronous call is made inside the main method and the method will finish after the main method finishes.

I have some slides about how to handle concurrency on apple os: https://speakerdeck.com/yageek/concurrency-on-darwin.

Old answer: You could try the dispatch_group_t. You can think them as retain counter for GCD.

Imagine the code below in the main method of your NSOperation subclass :

- (void) main
{

   self.executing = YES;
   self.finished = NO;

   // Create a group -> value = 0
   dispatch_group_t group = dispatch_group_create();

   NSURLSession * session = [NSURLSession sharedInstance];

   NSURL *url = [NSURL URLWithString:@"http://someurl"];

    // Enter the group manually -> Value = Value + 1
   dispatch_group_enter(group); ¨

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


      /* Do your stuff here */

      NSLog("Will show in first");

      //Leave the group manually -> Value = Value - 1
      dispatch_group_leave(group);
   }];

   [dataTask resume];

  // Wait for the group's value to equals 0
  dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

  NSLog("Will show in second");

  self.executing = NO;
  self.finished = YES;
}
yageek
  • 4,115
  • 3
  • 30
  • 48
  • 1
    You don't need dispatch_group, you don't have a group here, what you need is a semaphore. – pronebird Aug 07 '15 at 11:48
  • While I didn't know about the `asynchronous` property, it's actually not having any effect for operations added to a queue, only for when operations are executed manually. From the docs: "When you add an operation to an operation queue, the queue ignores the value of the asynchronous property and always calls the start method from a separate thread.". https://developer.apple.com/documentation/foundation/nsoperation?language=objc – Koen. Dec 12 '20 at 21:43
  • Also, `executing` and `finished` are read-only properties; you should redefine those properties control their values. – Koen. Dec 12 '20 at 21:44
2

With NSURLSession you don't manually add any operations to a queue. You use the method - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request on NSURLSession to generate a data task which you then start (by calling the resume method).

You are allowed to provide the operation queue so you can control the properties of the queue and also use it for other operations if you wanted.

Any of the usual actions you would want to take on a NSOperation (i.e. start, pause, stop, resume) you perform on the data task.

To queue up 50 images to download you can simply create 50 data tasks which the NSURLSession will properly queue up.

Reid Main
  • 3,394
  • 3
  • 25
  • 42
  • How do I identify specific `NSURLSessionTask`s? In your example will the 50 run in the background (off the main thread)? Will they spawn multiple threads? And if so, will I have problems accessing the delegate methods? – Doug Smith Feb 20 '14 at 22:27
  • 1
    You need to track the tasks yourself. You could keep a dictionary of them where the key is the URL you are loading or something. The 50 tasks will run in background threads and the delegate methods will get called back on the queue you pass into the NSURLSession constructor. Remember that a operation queue can run on one or more threads. – Reid Main Feb 21 '14 at 16:25
  • 1
    @ReidMain "You are allowed to provide the operation queue so you can control the properties of the queue." I believe this is incorrect. You can provide the _delegate queue_, which controls where completion callbacks are made. But I don't think you can provide, access, or modify the internal queue that performs the network operations themselves. Unless I'm missing something? – algal Apr 20 '16 at 18:50
  • @algal NSURLSession _is_ a queue. Create a session when you want to make a bunch of requests, if you get an error after one of them and want to abandon the queue just call invalidateAndCancel, or getTasksWithCompletionHandler and cancel them all. Requests are done serially if you pass nil for the delegateQueue, if you want simultaneous then supply an operation queue. – malhal Sep 03 '16 at 23:26
  • @malhal "Requests are done serially if you pass nil for the delegateQueue, if you want simultaneous then supply an operation queue" Really? Are you sure? I don't see why the concurrency factor of the queue for delegate callbacks would affect the concurrency factor of `NSURLSession`'s own internal task queue? At least for completion callbacks, I would think independent queues allows for unrelated concurrency choices for each queue. – algal Sep 08 '16 at 16:11
  • 1
    I suppose what you have to do is create a session, then create each task using that session, then in each task's completion handler call the next task's resume method. – malhal Sep 08 '16 at 16:20
0

If you're using OperationQueue and don't want each operation to create many simultaneous network requests, you can simply call queue.waitUntilAllOperationsAreFinished() after each operation is added to the queue. They will now only execute after the previous one is completed, significantly reducing the amount of simultaneous network connections.

Above The Gods
  • 175
  • 2
  • 13
0

Here's a sample project using NSOperation with NSURLSession: https://github.com/MacMark/Operations-Demo

For background sessions you can resurrect the NSOperation by using the session's identifier in this callback:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
   // Restore the NSOperation that matches the identifier
   // Let the NSOperation call the completionHandler when it finishes
}
MacMark
  • 6,239
  • 2
  • 36
  • 41
-1

Maybe you are looking for this:

http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/

It is a bit weird that this isn't 'builtin', but if you want to hook up NSURL stuff with NSOperation's it looks like you have to reuse the runloop in the main thread and make the operation a 'concurrent' one ('concurrent' to the queue).

Though in your case - if it's just about plain downloads, with no subsequent, dependent, operations hooked up - I'm not sure what you would gain with using NSOperation.

hnh
  • 13,957
  • 6
  • 30
  • 40
  • I agree that its a bit weird that it's not built in, but I think this is a sacrifice made for background sessions, for which an operation-based implementation doesn't make sense (i.e. the tasks continue after your app is terminated). But for foreground tasks, wrapping it in `NSOperation` is not only possible, but IMHO, advisable. – Rob May 23 '14 at 19:19
  • 1
    "I'm not sure what you would gain with using NSOperation." - The main advantage of using operation queues is that you can control the degree of concurrency (`maxConcurrentOperationCount`) thereby eliminating the timeout problems that can plague code that is issuing many concurrent requests on a slow network connection. – Rob May 23 '14 at 21:48
  • 1
    BTW, there is a significant oversight in the blog post you reference, that he's not handling cancelled operations (which is one of the main reasons we use operation queue implementation rather than GCD). See the latter part of [this answer](http://stackoverflow.com/a/22261167/1271826) for some important updates on that blog article's code samples. – Rob Jun 09 '14 at 07:31