PDA

View Full Version : [Resolved] Multitasking Background Tasks




newtoiphonesdk
Mar 13, 2012, 01:00 PM
In my app with Asynchronous downloads, I am trying to implement multitasking to allow background tasks to complete, but am getting errors. In the AppDelegate didEnterBackground:

application = [UIApplication sharedApplication]; //Get the shared application instance

__block UIBackgroundTaskIdentifier background_task; //Create a task object

background_task = [application beginBackgroundTaskWithExpirationHandler: ^ {
[application endBackgroundTask: background_task]; //Tell the system that we are done with the tasks
background_task = UIBackgroundTaskInvalid; //Set the task to be invalid

//System will be shutting down the app at any point in time now
}];

//Background tasks require you to use asyncrous tasks

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Perform your tasks that your application requires

NSLog(@"\n\nRunning in the background!\n\n");

[application endBackgroundTask: background_task]; //End the task so the system knows that you are done with what you need to perform
background_task = UIBackgroundTaskInvalid; //Invalidate the background_task
});

I start a download, and then click the home button. The download doesn't terminate, but it doesn't keep going. Once I go back to the app it resumes. Then, when the app is finished: if it was sent to background at any point during the download, I get the following error in Console:


2012-03-13 12:17:25.981 SMCoC[16086:17f03] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x10ce80f0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key response.'
*** First throw call stack:

If I allow the download to finish without ever entering background, all is fine.



seepel
Mar 14, 2012, 12:55 AM
The code you posted looks like it should just throw the NSLog and finish the background task. Is there other code you removed?

The error seems unrelated to background tasks. At some point it seems like some code is getting run like this.


[someObject valueForKey:@"response"];


And that someObject variable is an NSString. Maybe you are trying to reference an invalid pointer that now happens to point to a string?

newtoiphonesdk
Mar 14, 2012, 10:21 AM
The code you posted looks like it should just throw the NSLog and finish the background task. Is there other code you removed?

The error seems unrelated to background tasks. At some point it seems like some code is getting run like this.


[someObject valueForKey:@"response"];


And that someObject variable is an NSString. Maybe you are trying to reference an invalid pointer that now happens to point to a string?

I didn't remove anything, and I found that code example on several different sites.

As far as the key "response",
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
progress.hidden = NO;
downloadInProgress = YES;
[receivedData setLength:0];
expectedBytes = [response expectedContentLength];
}
- (NSCachedURLResponse *) connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return nil;
}

Those are the only instances that use response. What would likely need to go in with the NSLog to allow the completion of the download?

seepel
Mar 15, 2012, 12:33 AM
Don't see anything immediately wrong here. I would try to isolate the problem. Start by just running a background process, so just dont end the process. Kick it off and don't do anything. The slowly add the rest back in and see what breaks.

newtoiphonesdk
Mar 15, 2012, 11:52 AM
Don't see anything immediately wrong here. I would try to isolate the problem. Start by just running a background process, so just dont end the process. Kick it off and don't do anything. The slowly add the rest back in and see what breaks.

Still going the same. I enter background, get the notification it has entered the background, but when I resume, the download didn't continue while in background. It will resume, and then when it finishes, it crashes the app. It was suggested that in my appdelegate didEnterBackground there is code missing, but the tutorials I have found it looks complete.

seepel
Mar 15, 2012, 09:23 PM
I think I understand now. When you launch your background task you immediately end it. I think you want to wait until the download is finished or returns with an error before you end your background task.

newtoiphonesdk
Mar 15, 2012, 10:52 PM
I think I understand now. When you launch your background task you immediately end it. I think you want to wait until the download is finished or returns with an error before you end your background task.

Pardon my ignorance, this is my first attempt at Asynchronous downloads, and multitasking for background processes, so I am not for sure how to go about doing this. Is there somewhere that would lead me in the right direction of how to do this?

In this link (http://jialing.name/xcode-4/ios-multitasking-background-tasks/) it shows implementing backgrounding a task, but stops short of anything teaching how to put it in there.

seepel
Mar 16, 2012, 02:42 PM
Alright, I'm finally at my computer when replying so I think I can give a better example with code. You'll want to store the background task identifier in a variable in the class. I've added hopefully make things more clear. My comments are in bold. Ultimately you want to wait until NSURLConnection completes before ending the background task.

When you call beginBackgroundTaskWithExpirationHandler: all you are doing is telling the OS "Hey! I want to run in the background for a while so don't shut me down when the user exits!"

Then when you call endBackgroundTask: you are telling the OS "Hey forget about that whole thing where I want to be run in the background, I'm finished now!"

Add this to the header....

// EDIT this should not be a pointer! I'll leave it here so the thread makes sense but this line should be
// @property (nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
@property (nonatomic) UIBackgroundTaskIdentifier *backgroundTaskIdentifier;


And in the implementation...

@synthesize backgroundTaskIdentifier;

- (void)launchDownload {
NSURL *URL = [NSURL URLWithString:@"http://myurl.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
// __block signals that the variable should be available inside blocks.
__block NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];

UIApplication *application = [UIApplication sharedApplication]; //Get the shared application instance

__block UIBackgroundTaskIdentifier background_task; //Create a task object

background_task = [application beginBackgroundTaskWithExpirationHandler: ^ {
// This code gets called when your app has been running in the background too long and the OS decides to kill it
// You might want to cancel your connection in this case, that way you won't receive delegate methods any longer.
[connection cancel]
[application endBackgroundTask: background_task]; //Tell the system that we are done with the tasks
background_task = UIBackgroundTaskInvalid; //Set the task to be invalid

//System will be shutting down the app at any point in time now
}];

self.backgroundTaskIdentifier = background_task;



//Background tasks require you to use asyncrous tasks

// The following code simply prints a log message an another thread and then ends the background task.
// I don't think you actually want this here
// You are already using the asynchronous NSURLConnection
//dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// //Perform your tasks that your application requires
//
// NSLog(@"\n\nRunning in the background!\n\n");
//
// The following line tells the OS that the background task is finished and it can shut down the app.
// I think having this line here means that the app closes before the connection finishes
// [application endBackgroundTask: background_task]; //End the task so the system knows that you are done with what you need to perform
// background_task = UIBackgroundTaskInvalid; //Invalidate the background_task
//});
}

// You'll want to end the background process when your connection is done loading
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// Do the things you need to do if the connection failed, here is an option log message
NSLog(@"Connection failed with error: %@, %@", error, error.userInfo);
// End the background process
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Do the things you need to do when the connection finishes
// End the background process
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}

newtoiphonesdk
Mar 16, 2012, 07:10 PM
I think I am understanding this, but have a question, and it may come out wrong or seem stupid, so I apologize if that is the case. The download occurs in a TableView and not in the AppDelegate file. Do I need to restructure the class implementation codes to call the download request in the AppDelegate?

Basically, TableView Class code is where all the NSURLConnection requests are at, and I noticed you had the finish one loaded in the AppDelegate. Do they all need to be in the same class?

seepel
Mar 16, 2012, 10:49 PM
I think I am understanding this, but have a question, and it may come out wrong or seem stupid, so I apologize if that is the case. The download occurs in a TableView and not in the AppDelegate file. Do I need to restructure the class implementation codes to call the download request in the AppDelegate?

Basically, TableView Class code is where all the NSURLConnection requests are at, and I noticed you had the finish one loaded in the AppDelegate. Do they all need to be in the same class?

No you don't need to do any of this in the AppDelegate, I don't think any of the code I posted had anything to do with an AppDelegate.

newtoiphonesdk
Mar 18, 2012, 04:20 PM
No you don't need to do any of this in the AppDelegate, I don't think any of the code I posted had anything to do with an AppDelegate.

Tried this out and it came back with some warnings and errors:
Incompatible pointer to integer conversion sending 'UIBackgroundTaskIdentifier *' (aka 'unsigned int *') to parameter of type 'UIBackgroundTaskIdentifier' (aka 'unsigned int'); dereference with *

I tried to use the Fix-It in Xcode to run the app, and came back with the following for my download code method:

RSSEntry *entry = [_allEntries objectAtIndex:indexPath.row];

self.nameit = entry.articleTitle;



NSURL *url = [NSURL URLWithString:entry.articleUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// __block signals that the variable should be available inside blocks.
__block NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];

UIApplication *application = [UIApplication sharedApplication]; //Get the shared application instance

__block UIBackgroundTaskIdentifier background_task; //Create a task object

background_task = [application beginBackgroundTaskWithExpirationHandler: ^ {
// This code gets called when your app has been running in the background too long and the OS decides to kill it
// You might want to cancel your connection in this case, that way you won't receive delegate methods any longer.
[connection cancel];
[application endBackgroundTask: background_task]; //Tell the system that we are done with the tasks
background_task = UIBackgroundTaskInvalid; //Set the task to be invalid

//System will be shutting down the app at any point in time now
}];

self.backgroundTaskIdentifier = &(background_task);









if (connection) {
receivedData = [[NSMutableData data] retain];
self.thetable = tableView;
self.thepath = indexPath;
}
else {
UIAlertView *cancelled = [[UIAlertView alloc] initWithTitle:@"Download Failed" message:@"Please check your network settings, and then retry the download." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[cancelled show];
[cancelled release];
}

Then, for the other methods you mentioned:
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[connection release];
UIAlertView *connectionfailed = [[UIAlertView alloc] initWithTitle:@"Download Failed" message:@"Please check your network settings, and then retry the download." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[connectionfailed show];
[connectionfailed release];
[[UIApplication sharedApplication] endBackgroundTask:*(self.backgroundTaskIdentifier)];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
progress.hidden = YES;
downloadInProgress = NO;
[downloadlabel removeFromSuperview];
[thetable deselectRowAtIndexPath:thepath animated:YES];




}

&
- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pdfPath = [documentsDirectory stringByAppendingPathComponent:[nameit stringByAppendingString:@".mp3"]];
NSLog(@"Succeeded! Received %d bytes of data",[receivedData length]);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
[receivedData writeToFile:pdfPath atomically:YES];
progress.hidden = YES;
downloadInProgress = NO;
[downloadlabel removeFromSuperview];

[thetable deselectRowAtIndexPath:thepath animated:YES];

[connection release];
}

But, it still terminated when trying to enter background while downloading.
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x8847b30> valueForUndefinedKey:]: this class is not key value coding-compliant for the key response.'

seepel
Mar 18, 2012, 10:09 PM
Well shucks. The backgroundTaskIdentifier shouldn't be a pointer. That's my mistake. As far as the exception goes it must be hiding elsewhere. Have you been releasing the connection from the beginning? I don't think you want to do that unless you explicitly retain it somewhere else. By the way did you do a stack trace? Turn on debugging and when the exception gets thrown type bt into the console.

newtoiphonesdk
Mar 18, 2012, 11:06 PM
Well shucks. The backgroundTaskIdentifier shouldn't be a pointer. That's my mistake. As far as the exception goes it must be hiding elsewhere. Have you been releasing the connection from the beginning? I don't think you want to do that unless you explicitly retain it somewhere else. By the way did you do a stack trace? Turn on debugging and when the exception gets thrown type bt into the console.

Ok, the pointer problem was definitely an issue with that bit of code, however, after I changed it, a couple of seconds into the background while downloading crashed the app and returned the error and stack trace:
2012-03-18 23:05:04.282 SMCoC[35033:17f03] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x10730e50> valueForUndefinedKey:]: this class is not key value coding-compliant for the key response.'
*** First throw call stack:
(0x9ea022 0x2570cd6 0x9e9ee1 0x1032efe 0xfa1831 0xfa0c99 0xfa4866 0x61e32 0x62401 0x9ebe42 0x7fc2b 0x9ebe42 0xfab9df 0x9be94f 0x921b43 0x921424 0x920d84 0x920c9b 0x27727d8 0x277288a 0x1579626 0x21fd 0x2175)
terminate called throwing an exception[Switching to process 35033 thread 0x17f03]
sharedlibrary apply-load-rules all
(gdb) bt
#0 0x948c19c6 in __pthread_kill ()
#1 0x90c2ef78 in pthread_kill ()
#2 0x90c1fbdd in abort ()
#3 0x0251ee78 in abort_message ()
#4 0x0251c89e in default_terminate ()
#5 0x02570f17 in _objc_terminate ()
#6 0x0251c8de in safe_handler_caller ()
#7 0x0251c946 in std::terminate ()
#8 0x0251db3e in __cxa_rethrow ()
#9 0x02570e15 in objc_exception_rethrow ()
#10 0x00920de0 in CFRunLoopRunSpecific ()
#11 0x00920c9b in CFRunLoopRunInMode ()
#12 0x027727d8 in GSEventRunModal ()
#13 0x0277288a in GSEventRun ()
#14 0x01579626 in UIApplicationMain ()
#15 0x000021fd in main (argc=1, argv=0xbffff4b4) at /Users/MYUSERNAME/Desktop/XCodeProjects/SpringMeadowsipad/main.m:13
Current language: auto; currently objective-c
(gdb)

I don't know if this will help, but here is the entire code for method didSelectRowAtIndexPath for my TableView. In it, I use block based action sheet for several methods. Options are to download MP3 or simply to load the URL into a webview to play it. If download is chosen, it checks if downloads are in progress (to limit downloads to one at a time) and if file has already been downloaded (to avoid wasting bandwidth), and if neither of those are true, then it begins downloading.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
BlockBasedActionSheet *askSheet =
[[BlockBasedActionSheet alloc]
initWithTitle:@"What Do You Want To Do" cancelButtonTitle:@"Cancel" downloadButtonTitle:@"Download" streamButtonTitle:@"Stream" cancelAction:^ {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}downloadAction:^ {
RSSEntry *entry = [_allEntries objectAtIndex:indexPath.row];

self.nameit = entry.articleTitle;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pdfPath = [documentsDirectory stringByAppendingPathComponent:[nameit stringByAppendingString:@".mp3"]];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pdfPath];

if (fileExists) {
NSLog(@"You already got that one dummy!");
UIAlertView *ditto = [[UIAlertView alloc] initWithTitle:@"Already Downloaded" message:@"You have already downloaded this sermon." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[ditto show];
[ditto release];
[tableView deselectRowAtIndexPath:indexPath animated:YES];

}
if (!fileExists) {

if (downloadInProgress) {
UIAlertView *inprogress = [[UIAlertView alloc] initWithTitle:@"Busy" message:@"Please let your previous download finish before starting another." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[inprogress show];
[inprogress release];
[tableView deselectRowAtIndexPath:indexPath animated:YES];

}
if (!downloadInProgress) {
RSSEntry *entry = [_allEntries objectAtIndex:indexPath.row];

self.nameit = entry.articleTitle;
NSURL *url = [NSURL URLWithString:entry.articleUrl];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];
__block NSURLConnection *connection = [NSURLConnection connectionWithRequest:theRequest delegate:self];

UIApplication *application = [UIApplication sharedApplication]; //Get the shared application instance

__block UIBackgroundTaskIdentifier background_task; //Create a task object

background_task = [application beginBackgroundTaskWithExpirationHandler: ^ {
// This code gets called when your app has been running in the background too long and the OS decides to kill it
// You might want to cancel your connection in this case, that way you won't receive delegate methods any longer.
[connection cancel];
[application endBackgroundTask: background_task]; //Tell the system that we are done with the tasks
background_task = UIBackgroundTaskInvalid; //Set the task to be invalid

//System will be shutting down the app at any point in time now
}];

self.backgroundTaskIdentifier = background_task;
if (connection) {
receivedData = [[NSMutableData data] retain];
self.thetable = tableView;
self.thepath = indexPath;
}
else {
UIAlertView *cancelled = [[UIAlertView alloc] initWithTitle:@"Download Failed" message:@"Please check your network settings, and then retry the download." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[cancelled show];
[cancelled release];
}
}
}

}streamAction:^ {
Reachability *reachability = [Reachability reachabilityWithHostName:@"google.com"];
NetworkStatus status = [reachability currentReachabilityStatus];

if (status == ReachableViaWiFi) {

if (_webViewController == nil) {
self.webViewController = [[[WebViewController alloc] initWithNibName:@"WebViewController" bundle:[NSBundle mainBundle]] autorelease];
}
RSSEntry *entry = [_allEntries objectAtIndex:indexPath.row];
_webViewController.entry = entry;
[self.navigationController pushViewController:_webViewController animated:YES];
}
else {
UIAlertView *notonwifi = [[UIAlertView alloc] initWithTitle:@"Wifi Required" message:@"You need to be connected to a Wifi network to stream the sermons. Please turn on wifi in Settings and try again." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[notonwifi show];
[notonwifi release];
}
}];
[askSheet showInView:self.tabBarController.view];
[askSheet release];












}

newtoiphonesdk
Mar 19, 2012, 10:08 AM
This error may have nothing to do with the attempts at background downloading. I removed all code I added to try and background, and the same error comes up when I go to background, and then come back to foreground. I really cannot figure out what is going on here.

EDIT: The error that I was getting was caused by Required Background Modes being set to App Plays Audio in the .plist. I'm not 100% sure why having that key created the error, but it did.

UPDATE: I tried removing Required Background Modes and leaving in the code to download the file in the background, and still got the error. It appears that trying to add any kind of background on this app will terminate the app when it returns to the foreground, with the kvc error for response. I have looked everywhere for any other key of response, and cannot find one anywhere, and I double-checked my .plist to make sure that I was not disallowing the app to run in background. I am truly lost as to what is going on.

newtoiphonesdk
Mar 19, 2012, 07:27 PM
Ok, so I started rebuilding from scratch and apparently there was an issue for the push notification library and/or JSON library I was using. Now, when I start a download and enter background, the download will finish, but then immediately crash. In the console, I get the Succeeded! Downloaded xbytes message, and I also get the message:
Should've been invalidated
right before the crash, though I have no idea where that NSLog is set, and there are no more clues in console given to where the problem lies.

seepel
Mar 19, 2012, 09:27 PM
What type of crash s it? There is this stack overflow post that sounds kind of similar http://stackoverflow.com/questions/8953635/app-crashing-when-main-returns-with-shouldve-been-invalidated

newtoiphonesdk
Mar 19, 2012, 10:45 PM
What type of crash s it? There is this stack overflow post that sounds kind of similar http://stackoverflow.com/questions/8953635/app-crashing-when-main-returns-with-shouldve-been-invalidated

Thread1: Program received signal: "EXC_BAD_ACCESS".

All console gave me was:
2012-03-19 22:41:01.473 SMCoC[5531:707] Succeeded! Received 7109723 bytes of data
2012-03-19 22:41:01.856 SMCoC[5531:707] Should've been invalidated
[Switching to process 7171 thread 0x1c03]
(gdb) bt
#0 0x31bb5fbc in objc_msgSend ()
#1 0x347828fa in URLConnectionClient::releaseClientLocked ()
#2 0x3477737e in URLConnectionClient::processEvents ()
#3 0x34777178 in MultiplexerSource::perform ()
#4 0x374c1b02 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#5 0x374c12ce in __CFRunLoopDoSources0 ()
#6 0x374c0074 in __CFRunLoopRun ()
#7 0x374434dc in CFRunLoopRunSpecific ()
#8 0x374433a4 in CFRunLoopRunInMode ()
#9 0x315f9fcc in GSEventRunModal ()
#10 0x32ab5742 in UIApplicationMain ()
#11 0x00003a7c in main (argc=1, argv=0x2fdffac4) at /Users/candace.brassfield (Deleted)/Desktop/XCodeProjects/SpringMeadowsipad/main.m:13
(gdb)

The download finishes while in the background...as referenced by the Succeeded! Received 7109723 bytes of data from the NSLog. The crash occurs when coming back into the foreground.

In regards to the link posted...I am not certain about Zombie objects, or how to find them. Would this be in regards to where or when I release connection?

EDIT: Enabled Zombie Objects in schemes for debugging and returned the following error:
*** -[NSURLConnectionInternalConnection _withConnectionDisconnectFromConnection]: message sent to deallocated instance 0x87746a0

UPDATE: Connection was being released in the wrong spot. Fixed that and it downloaded in background and resumed perfectly. Your original reply (minus the pointer issue) was dead-on correct...sorry for my 3rd party JSON library throwing issues and monkey wrenches in it.

seepel
Mar 20, 2012, 11:51 PM
Glad to hear things are sorted out.