Chaining background NSURLSession uploads - ios

Has anyone been successful in chaining NSURLSession background uploads?
I am trying to upload a huge video file in 5 MB parts using background upload of NSURLSession. The uploads has to be in order. The whole thing works fine in foreground. I am using AFNetwoking for this, and its a multi part upload. But when the app is in background, the first item uploads fine and starts the second one in background (in setDidFinishEventsForBackgroundURLSessionBlock of AFURLSessionManager). But it stops abruptly (my best guess is in 30 seconds, as an app woken up in background has a max lifetime of 30 sec) and then nothing happens. I expected the second session will finish in background and call up the third etc - a chain behaviour, but this just does not seem to work.
I have tried adding all file parts to a single NSURLSession in one go with a HTTPMaximumConnectionsPerHost = 1 - this works fine and uploads the full file in parts. But the file parts are picked in random order, i.e. part 1 gets uploaded, then part 5, part 3, part 10 etc …. I tried adding this in an NSOperationQueue with dependency between operations and this seems to mess up the entire thing - the upload does not work at all.
I know that the video file can be uploaded as a single file in background, but the server expects this in 5 MB parts. Hence I guess my only option is to chain uploads, or add all the parts to a NSURLSession, but make sure they are always uploaded in the order they are added.
Any help would be appreciated.
My code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:#"%d", rand()]];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
config.HTTPMaximumConnectionsPerHost = 1;
[manager setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
dispatch_async(dispatch_get_main_queue(), ^{
// Call the completion handler to tell the system that there are no other background transfers.
// completionHandler();
[self upload];
});
}];
}
- (IBAction)start:(id)sender {
[self upload];
}
-(void) upload {
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Sample" ofType:#"mp4"];
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:#"234", #"u", #"Sample.mp4", #"f",nil];
NSMutableURLRequest *request = [serializer multipartFormRequestWithMethod:#"POST" URLString:urlString parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath] name:#"data" fileName:#"Sample.mp4" mimeType:#"video/mp4" error:nil];
} error:nil];
__block NSString *tempMultipartFile = [NSTemporaryDirectory() stringByAppendingPathComponent:#"Test"];
tempMultipartFile = [tempMultipartFile stringByAppendingString:[NSString stringWithFormat:#"%d", rand()]];
NSURL *filePathtemp = [NSURL fileURLWithPath:tempMultipartFile];
__block NSProgress *progress = nil;
[serializer requestWithMultipartFormRequest:request writingStreamContentsToFile:filePathtemp completionHandler:^(NSError *error) {
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePathtemp progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSLog(#"Request--> %#.\n Response --> %# \n%#", request.URL.absoluteString ,responseObject, error? [NSString stringWithFormat:#" with error: %#", [error localizedDescription]] : #""); //Lets us know the result including failures
[[NSFileManager defaultManager] removeItemAtPath:tempMultipartFile error:nil];
}];
[uploadTask resume];
[manager setTaskDidSendBodyDataBlock:^(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
NSLog(#"uploading");
}];
}];
}

Well, finally I reached out to Apple for clarifications on chaining background uploads - This is not possible in iOS.NSURLSession has a Resume Rate Limiter which prevents apps from executing chained tasks in background as explained in https://forums.developer.apple.com/thread/14854. Instead, apple suggests batch transfers or other options like https://forums.developer.apple.com/thread/14853. The other thing I was asking was to order the multiple tasks in upload queue - i.e, force NSURLSession to upload tasks in the order in which they are added. As pointed by dgatwood, using an NSOperationQueue is not possible and Apple also mentioned the same.As mentioned by eskimo in response mail "NSURLSession does not guarantee to run your requests in order and there’s no way to enforce that." So I am pretty much left option less on my original problem.

An NSOperationQueue goes away when your app does, which isn't too long after you put it into the background. So that's not going to work very well.
Instead, store a list of files remaining to upload, in order—either in a file on disk or in NSUserDefaults, depending on your personal preference. Then, use an upload task in a background session to start the first task. When it finishes, if your app isn't running, it should automatically get relaunched in the background to handle the data.
To support this behavior, in your application:handleEventsForBackgroundURLSession:completionHandler: method, re-create the background session just like you did originally, and store the completion handler.
Shortly thereafter, your delegate methods for the request should be called just as though your app were still running when the download finished. Among other things those methods can provide your app with the response data from the server, the response object (for checking the status code), etc.
When you get the didCompleteWithError delegate call (which is nil on success, IIRC), if the transfer failed, try it again or whatever. If it succeeded, start uploading the next one and update your list of files on disk.
Either way, when your session delegate's ** URLSessionDidFinishEventsForBackgroundURLSession:** method is called, call the handler you stored earlier, roughly like this:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
storedHandler();
}];
By calling the completion handler, you're telling the OS that you don't need to keep running.
Rinse, repeat.
If your app is still running when the request completes, everything happens just as described above, except that you don't get the application:handleEventsForBackgroundURLSession:completionHandler: or URLSessionDidFinishEventsForBackgroundURLSession: calls, which means you don't have to store the completion handler or call it.
See URL Session Programming Guide for details.

Related

error back when using nsurlsession to access a webservice several times in a for loop

I need to get image information from server, such image name, image id. Then use image id as one of parameters to make post, get image actual data. More specific, there are three images I should get.
First, I use getImageInfo to get image information.
- (void)getImageInfo {
// compose request
NSUserDefaults *getUserInfo = [NSUserDefaults standardUserDefaults];
NSString *uid = [getUserInfo objectForKey:#"uid"];
NSString *checkCode = [getUserInfo objectForKey:#"checkCode"];
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#", uid, checkCode];
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetallshibietu"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPMethod = #"POST";
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
if (!error) {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
// parse data in ram and put into images' imageInfos array
[self.images parseImageInfo:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
[self getImageRawData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
}] resume];}
Then I use getImageRawData to get three image data.
- (void)getImageRawData {
// compose request dynamically
NSUserDefaults *getUserInfo = [NSUserDefaults standardUserDefaults];
NSString *uid = [getUserInfo objectForKey:#"uid"];
NSString *checkCode = [getUserInfo objectForKey:#"checkCode"];
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetthetupian"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = #"POST";
NSInteger count = 0;
for (ImageInformation *temp in self.images.imageInfos) {
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#&tupianid=%#", uid, checkCode, temp.imageId];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
// if client side is no errors, continue
if (!error) {
// if server side is no errors, continue
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"图片内容:%#", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// in ram and put into images' imageRawData array
[self.images parseImageRawData:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] withImageId:temp.imageId withIndex:count];
// store data to disk
// NSString *path = [[NSString alloc] initWithFormat:#"image%#", temp.imageId];
// [FCFileManager writeFileAtPath:path content:data];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
}] resume];
count++;
}}
Here, it will loop three times, three responses come back, only the last one is complete, the others carry a error message, or incomplete raw data sometimes. Now I'm diving into concurrency programming guide, I guess serial queue likely can solve this problem.
Output like this:
2014-12-16 22:38:48.739 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>error</ns:return></ns:serviceGetthetupianResponse>
2014-12-16 22:38:48.749 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>error</ns:return></ns:serviceGetthetupianResponse>
2014-12-16 22:38:51.943 WeddingNewVersion[997:83366] 图片内容:<ns:serviceGetthetupianResponse xmlns:ns="http://serviceimpl.my.com"><ns:return>/9j/...(complete data)...9k=%%226654474.0</ns:return></ns:serviceGetthetupianResponse>
parameters of requests:
2014-12-17 14:59:25.364 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=41
2014-12-17 14:59:25.368 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=42
2014-12-17 14:59:25.368 WeddingNewVersion[1875:226651] uid=6&yangzhengma=odWoDXWcBv1jOrEhywkq7L&tupianid=43
the problem is likely not in composing request.
------------------------------------------------update1-----------------------------------------------
I have tried to put data task of session into a serial queue. Disappointed, this is not working.
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){...}] resume];
});
Meanwhile, I make delegateQueue of session as nil, reference says if nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
Now I am still confused how to make it right.
-----------------------------------------------update2------------------------------------------------
I add [NSThread sleepForTimeInterval:0.5] into the block dispatched to serial queue.
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){...}] resume];
[NSThread sleepForTimeInterval:0.5];
});
It does not work. The three responses are complete, but they are all the same.
Thank you in advance!
I'm just guessing as I've never tried it, but possibly your data tasks are all using the same TCP port on your end.
That would be OK if they were serialized - one after the other, in sequence - but if they overlap, then the server would receive garbled HTTP requests:
GET /foo
GET /bar
GET /baz
What the server would see might be something like:
GET /fGET /baroo
GET /baz
That your third requests actually works OK might be an accident of the timing.
If you absolutely require the three requests to be issued simultaneously, there are ways to open three different ports on your end. I don't know how to do it with Cocoa and Objective-C, but you can certainly do it with C and Berkeley Socket system calls. The Cocoa / Cocoa Touch networking methods are just wrappers around sockets.
A couple of thoughts:
Your technique of using a single NSMutableURLRequest instance, and repeatedly mutating it for each request (while the prior requests are still in progress) is curious.
In the spirit of thread safety, I would use a separate NSMutableURLRequest for each concurrent request. You don't want to risk having your thread issuing these requests mutate the request object while some background thread performing one of the prior requests. (See Apple's Thread Safety Summary in the Threading Programming Guide in which they point out that mutable classes are not generally thread safe.)
Having said that, the NSURLConnection documentation leaves us with the impression that this request object would be copied, mitigating this problem. I don't see this sort of assurance in the NSURLSession documentation (though I suspect it does the same thing).
I don't think this is the problem here (if this was the problem, the problem would likely be more erratic than what you report, and besides, I suspect that NSURLSession is handling this gracefully, anyway), but as a matter of good thread-safe coding habits, it would be prudent to let each concurrent request have its own NSMutableURLRequest object.
You have confirmed that the information being used in the requests looks valid.
If you wanted to take this to the next level, you might use Charles (or Wire Shark or whatever tool you prefer) to observe the actual requests as they go out. These sorts of tools are invaluable for debugging these sorts of problems.
If you observe the requests in Charles and confirm that they are valid, then this categorically eliminates client-side issues from the situation.
What is curious is that you are not receiving NSError object from dataTaskWithRequest. Nor are you receiving statusCode other than 200 from your server. That means that your requests were successfully sent to the server and received by the server.
Instead, the server is processing the request, but is having a problem fulfilling the request. This leads me to wonder about the server code, itself. I suspect that there is something in the server code that is preventing concurrent operations from taking place (e.g., locking some shared resource, such as temp file or SQL table, for the duration of the request). I would take a hard look at the server code and make sure there are no potential contention issues.
Furthermore, I would modify the server code to not simply report "error", but rather to produce a meaningful error message (e.g. system provided error messages, error codes, etc.). Your server is detecting an error, so you should have it tell you precisely what that error was.
Note, I am explicitly not advising you to make your requests run sequentially. That is inadvisable. While it might solve the immediate problem, you pay a huge performance penalty doing that, and it's not scalable. And remember, you really must handle concurrent requests gracefully, as you're likely to have multiple users of the app at some point.
I would take a hard look at the server code, adding further debugging information to the error messages in order to track down the problem.
I put request into for loop, it works. The first thought of rob about NSMutableRequest and NSURLSession seems right, I'm trying to catch the whole idea. Thanks for rob's answer. Anyway, this is code.
for (ImageInformation *temp in self.images.imageInfos) {
// compose request dynamically
NSURL *url = [NSURL URLWithString:#"http://121.199.35.173:8080/xihuan22dcloud/services/Shibietupianservice/serviceGetthetupian"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = #"POST";
NSString *data = [NSString stringWithFormat:#"uid=%#&yangzhengma=%#&tupianid=%#", uid, checkCode, temp.imageId];
request.HTTPBody = [data dataUsingEncoding:NSUTF8StringEncoding];
// data task
dispatch_async(self.serialQueue, ^{
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
// if client side is no errors, continue
if (!error) {
// if server side is no errors, continue
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
// in ram and put into images' imageRawData array
[self.images parseImageRawData:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] withImageId:temp.imageId];
// store data to disk
// [FCFileManager writeFileAtPath:path content:data];
// dispatch display image task to main
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.images.imageDrawDatasDic count] == [self.images.imageInfos count]) {
[self.tableView reloadData];
}
});
}
}
}] resume];
[NSThread sleepForTimeInterval:0.5];
});
}
}

How to update a UILabel synchronously with code in a method using setNeedsDisplay

I am downloading a zip file from the internet and unzipping it. The process takes 15 seconds or so. I want to display some sort of time elapsed to the user. As a start I just want to update a UILabel on my _countUpView UIView when the download has finished and the unzipping begins. I believe that I need to use setNeedsDisplay on the view that holds the UILabel but I can not seem to figure out what I am doing wrong, here is my code:
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSData *slicedata= [NSData dataWithContentsOfURL:url];
NSError *error;
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error:&error]){
NSLog(#"Create directory error: %#", error);
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
[self displayCountUpNOW];
NSLog(#"Unzipping");
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error];
if (error.code!= noErr){
NSLog(#"Unzip error");
return;
}else{
NSLog(#"Unzipped successfully");
}
And the setNeedsDisplay method is:
- (void) displayCountUpNOW {
dispatch_async(dispatch_get_main_queue(), ^{
[_countUpView setNeedsDisplay];
});
}
My label does not change to "Unzipping" until NSLog shows "Unzipped successfully", about 10 seconds after I set the label to "Unzipping".
In those 10 seconds, the unzipping occurs but a timer that I want to use to update the label every second stops executing too so I can't display the elapsed time in the label either.
Please help me understand this asynchronous environment.
Carmen
EDIT-EDIT-EDIT
The below code seems to work asynchronously and I even have my elapsed time indicator working since my timer isn't stopped. I couldn't find a method in SSZipArchive that works without a file so I left the save file in. How did I do Duncan? Thanks again, that is pretty slick!
ONE MORE QUESTION: What is the best way to know when an asynchronous request is still outstanding, by setting a global flag variable when the request is made and clearing it when the async process completes?
gotALL= 0;
_countUpLab.text= #"Downloading";
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *slicedata, NSError *error1){
if ([slicedata length]> 0 && error1== nil){
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
NSError *error2;
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error: &error2]){
NSLog(#"Create directory error: %#", error2);
[self endCountUp];
return;
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
NSLog(#"Unzipping");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error3;
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error3];
dispatch_async(dispatch_get_main_queue(), ^(void) {
if (error3.code!= noErr){
NSLog(#"Unzip error %#", error3.description);
[self endCountUp];
return;
}else{
NSLog(#"Unzipped successfully");
gotALL= 1;
[self endCountUp];
return;
}
});
});
}else if ([slicedata length]== 0 && error1== nil){
//todo yet
}else if (error1!= nil){
//todo yet
}
}];
Lots of problems with your code. First of all, Your current code will lock up the user interface until the download is complete. That is bad, even for short downloads. If there is a network problem, this can lock up the user interface for up to 2 minutes, even for a short file.
You should download the file using an async download, not synchronous.
Second of all, UI updates only get rendered to the screen when your code returns and the app visits the event loop. Code using this approach:
Set label text to "doing stuff"
Do something time-consuming
Change label text to "done with stuff"
Does not work. The text "doing stuff" never shows up in the label, because the label changes aren't drawn until your code finishes and returns, and by then, you've replaced the label text with "done with stuff."
Here's what you should do instead:
Use the NSURLConnection method sendAsynchronousRequest:queue:completionHandler:. Put the code that you want to run once the download is complete in the completionHandler block.
That method handles the background stuff for you, and then runs your completion block on the main thread once the download is complete. It's a slick system method.
It should be possible to do the unzipping from the background using dispatch_async. (I'm not familiar with the SSZipArchive library, so I'm not positive that it's thread-safe, but it should be.)
The basic idea is:
Display a "downloading" message.
Create an NSURLRequest.
Use sendAsynchronousRequest:queue:completionHandler: to submit the request asynchronously.
In the completion handler:
Change the message to "unzipping"
Save the downloaded data to disk if the unzip library requires that it be saved to a file. If the zip library can do the unzipping from memory, you can skip this step.
Use dispatch_async to unzip the file from the default priority global queue
At the end of the unzip block, use dispatch_async(dispatch_get_main_queue()) to change the label to "unzip complete" or whatever you want to say. (You have to send the message to the main queue because you can't change the UI from a background thread.)
Try and figure out how to code the above approach. If you get stuck, post what you've tried as an edit to your post and we can guide you, but it's better if you try to write this code yourself. You'll learn that way rather than copy/pasting.

AFNetworking and background transfers

I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSession background transfers features and AFNetworking (versions 2 and 3).
I saw the WWDC 705 - What’s New in Foundation Networking session, and they demonstrated background download that continues after the app terminated or even crashes.
This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler: and the fact that the session's delegate will eventually get the callbacks and can complete its task.
So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.
The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?
Or maybe I'm missing something here...
Let me explain what I mean:
For example my app is a photo messaging app, lets say that I have a PhotoMessage object that represent one message and this object has properties like
state - describe the state of the photo download.
resourcePath - the path to the final downloaded photo file.
So when I get a new message from the server, I create a new PhotoMessage object, and start downloading its photo resource.
PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;
self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *filePath = // some file url
return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
// update the PhotoMessage Object
newPhotoMsg.state = kStateDownloadFinished;
newPhotoMsg.resourcePath = filePath;
}
}];
[self.photoDownloadTask resume];
As you can see, I use the completion block to update that PhotoMessage object according to the response I get.
How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.
A couple of thoughts:
You have to make sure you do the necessary coding outlined in the Handling iOS Background Activity section of the URL Loading System Programming Guide says:
If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.
That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What’s New in Foundation Networking are even more clear.
The basic implementation of AFURLSessionManager will work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManager method where you create the NSURLSessionTask for uploads and downloads are lost "if the app terminated or crashes."
For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).
As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).
An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:
UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
// handle timeout gracefully if you can
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}];
But make sure that the completion block of the network task correctly informs iOS that it is complete:
if (taskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}
This is not as powerful as a background NSURLSession (e.g., you have a limited amount of time available), but in some cases this can be useful.
Update:
I thought I'd add a practical example of how to do background downloads using AFNetworking.
First define your background manager.
//
// BackgroundSessionManager.h
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "AFHTTPSessionManager.h"
#interface BackgroundSessionManager : AFHTTPSessionManager
+ (instancetype)sharedManager;
#property (nonatomic, copy) void (^savedCompletionHandler)(void);
#end
and
//
// BackgroundSessionManager.m
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "BackgroundSessionManager.h"
static NSString * const kBackgroundSessionIdentifier = #"com.domain.backgroundsession";
#implementation BackgroundSessionManager
+ (instancetype)sharedManager {
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
- (instancetype)init {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
self = [super initWithSessionConfiguration:configuration];
if (self) {
[self configureDownloadFinished]; // when download done, save file
[self configureBackgroundSessionFinished]; // when entire background session done, call completion handler
[self configureAuthentication]; // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
}
return self;
}
- (void)configureDownloadFinished {
// just save the downloaded file to documents folder using filename from URL
[self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
if (statusCode != 200) {
// handle error here, e.g.
NSLog(#"%# failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
return nil;
}
}
NSString *filename = [downloadTask.originalRequest.URL lastPathComponent];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingPathComponent:filename];
return [NSURL fileURLWithPath:path];
}];
[self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
if (error) {
// handle error here, e.g.,
NSLog(#"%#: %#", [task.originalRequest.URL lastPathComponent], error);
}
}];
}
- (void)configureBackgroundSessionFinished {
typeof(self) __weak weakSelf = self;
[self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
if (weakSelf.savedCompletionHandler) {
weakSelf.savedCompletionHandler();
weakSelf.savedCompletionHandler = nil;
}
}];
}
- (void)configureAuthentication {
NSURLCredential *myCredential = [NSURLCredential credentialWithUser:#"userid" password:#"password" persistence:NSURLCredentialPersistenceForSession];
[self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
if (challenge.previousFailureCount == 0) {
*credential = myCredential;
return NSURLSessionAuthChallengeUseCredential;
} else {
return NSURLSessionAuthChallengePerformDefaultHandling;
}
}];
}
#end
Make sure app delegate saves completion handler (instantiating the background session as necessary):
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], #"Identifiers didn't match");
[BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
}
Then start your downloads:
for (NSString *filename in filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
}
Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlock only.
Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.
It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlock with your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method:
URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.
One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.
Good luck!
AFURLSessionManager
AFURLSessionManager creates and manages an NSURLSession object based on a specified NSURLSessionConfiguration object, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.
link to documentation here documentation

handleEventsForBackgroundURLSession never called when a downloadTask finished

I am using AFURLSessionManager, and set the manager as a singleton instance.
- (AFURLSessionManager *)backgroundSession
{
static AFURLSessionManager *backgroundSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.testBackground.BackgroundDownload.BackgroundSession1234"];
backgroundSession = [[AFURLSessionManager alloc]initWithSessionConfiguration:config];
[backgroundSession setDownloadTaskDidWriteDataBlock:^(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite){
NSLog(#"i am downloading my id = %d progress= %f",downloadTask.taskIdentifier, totalBytesWritten*1.0/totalBytesExpectedToWrite);
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}];
[backgroundSession setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location){
NSLog(#"download finished");
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
return location;
}];
});
return backgroundSession;
}
//assign a download task
NSURLSessionDownloadTask *task = [manager1 downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return targetPath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog("%download success");
}];
[task resume];
I found that when I switch the app to the background the download task is running but when it was finished, the system call handleEventsForBackgroundURLSession will never be called.I am feeling that I have missed some setting or options. Any idea will be useful for me, thanks a lot.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
You probably have figured this out already but anyway, the exact same thing has happened to me while writing some test code to play with the background transfer service APIs (without AFNetworking). The solution was simply to change the identifier string for my background session configuration. Somehow the one I was using got bugged and the system wouldn't trigger the handleEventsForBackgroundURLSession callback. And not even restarting the device fixes it... however just changing the identifier does.
My theory is that my code created multiple instances of NSURLSession with the same configuration, which Apple clearly advices against (they say in the documentation that it has an undefined behavior if you do). I was obtaining the NSURLSessionConfiguration in the view controller's viewDidLoad method without a dispatch_once block, so it's certainly plausible that that happened.
As stated by Apple:
If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination. This behavior applies only for normal termination of the app by the system. If the user terminates the app from the multitasking screen, the system cancels all of the session’s background transfers. In addition, the system does not automatically relaunch apps that were force quit by the user. The user must explicitly relaunch the app before transfers can begin again.
Hope it helps.
Stefan

What's the best practice for running multiple tasks in iOS blocks and queues?

I've started to use blocks and queues heavily and they have been great. I use much less code and it is much easier to build and maintain. But I wonder about performance. In one case I am displaying a screen full of thumbnail images from a Flickr photo set. The code iterates over all items and starts a unique download queue to download each photo concurrently. It's working just fine, but I wonder if I should instead create a single static queue for downloading photos and then dispatch these download blocks to the same queue so that it can manage the blocks efficiently.
I've uploaded an example here.
http://www.smallsharptools.com/Downloads/iOS/UIImage+DownloadImage.zip
The implementation contents are also below. I appreciate any insight into better performance. (Later I'd like to handle caching for images by placing the file in the tmp folder so they are automatically cleared out periodically.)
How do you manage concurrent tasks with blocks? Do you create a static queue and dispatch blocks to the shared queue? Or does the implementation below implicitly manage all of my tasks efficiently already?
#import "UIImage+DownloadImage.h"
#implementation UIImage (DownloadImage)
+ (void)downloadImageWithURL:(NSURL *)imageURL andBlock:(void (^)(UIImage *image, NSError *error))returnImage {
dispatch_queue_t callerQueue = dispatch_get_current_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("Image Download Queue", NULL);
dispatch_async(downloadQueue, ^{
UIImage *image = nil;
NSError *error = nil;
// use the default cache policy to do the memory/disk caching
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:imageURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:15];
NSHTTPURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
// 200 indicates HTTP success
if (response.statusCode != 200) {
data = nil;
// set the error to indicate the request failed
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithFormat:#"Request failed with HTTP status code of %i", response.statusCode], NSLocalizedDescriptionKey, nil];
error = [NSError errorWithDomain:#"UIImage+DownloadImage" code:response.statusCode userInfo:userInfo];
}
else if (!error && data) {
image = [UIImage imageWithData:data];
}
// image will be nil if the request failed
dispatch_async(callerQueue, ^{
returnImage(image, error);
});
});
dispatch_release(downloadQueue);
}
#end
It does seem inefficient to create a 1-element queue each time, though I would be surprised if this would show up as a hotspot during profiling.
If you search on Apple's iOS forums, you should be able to find Quinn's discussion of using NSURLConnection "raw" rather than via threads.
You're doing synchronous network activity on queues. This seems like a rather poor idea, since you're blocking threads and forcing GCD to spin up new threads to service other blocks. If you're downloading 20 images simultaneously, then you will have 20 blocked threads in your app and another handful to actually do work. Instead you should be doing asynchronous network activity on a single worker thread. There's even a piece of Apple sample code that does this, though I cannot for the life of me remember what it's called.

Resources