I got a VoIP iOS App using a long polling mechanism to maintain its service connections and receive events (calls). This means, an NSURLConnection is pending for several minutes but will return immediately after an event occured. Due to the VoIP flag, it is possible to setup a keep alive handler and receive updates even while the app is in background mode.
However, this works most of the time but not reliably. Sometimes, NSURLConnection callbacks are heavily delayed or not fired at all, even after the request timed out (timeoutInterval of the NSURLRequest reached).
An example from the log to clarify:
The app runs in background mode (is launched by the system at boot time)
NSURLConnection #1 (long poll) is initiated and returns after 1 minute with some new data
NSURLConnection #2 (long poll) is initiated and returns after 15 minuted (server side maximum) without any new data
(...)
NSURLConnection #99 (long poll) is initiated but does NOT return - not even after timeoutInterval expired (16 minutes)
From time to time, the keep alive handler is called, bot nothing happens. The backgroundTimeRemaining property got an unrealistic high value (179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 instead of a maximum of 180.0).
After 1 hour, the user opens the app. The app is able to execute various NSURLRequests and receivce responses
After some secounds, the user closes the app
After 10 more minutes, the NSURLConnection #99 callback didFailWithError got fired with a timeout error (-1001). The exceution time of this request was more than one hour even tough the timeoutInterval was limited to 16 minutes and several other requests where initated later but completed earlier.
From my point of view, this seems like a very strange behaviour of iOS. Why should iOS give background excution time to the app and call the keep alive handler, but not properly fire the NSURLConnection callbacks in time?
Keep alive handler:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{
NSLog(#"########### Started Keep-Alive Handler ###########");
[self startBackgroundHandler:YES timeout:30];
NSLog(#"########### Completed Keep-Alive Handler ###########");
}];
[self startBackgroundHandler:NO timeout:60];
}
-(void)startBackgroundHandler:(BOOL)force timeout:(int)timeout {
UIApplicationState currentAppState = [[UIApplication sharedApplication] applicationState];
BOOL appIsBackground = currentAppState == UIApplicationStateBackground;
if(appIsBackground || force) {
int localThreadId = ++_currentBackgroundThreadId;
__block UIBackgroundTaskIdentifier bgTask;
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
NSLog(#"Cleaning up [Background Thread %d] ...", localThreadId);
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
NSLog(#"startBackgroundHandler with [Background Thread %d] appIsBackground=%d force=%d", localThreadId, appIsBackground, force);
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if(_currentBackgroundThreadId == localThreadId) {
NSLog(#"[Background Thread %d] Background time left: %0.1f", localThreadId, [UIApplication
sharedApplication].backgroundTimeRemaining);
sleep(timeout);
}
NSLog(#"[Background Thread %d] Will exit...", localThreadId);
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
} else {
NSLog(#"Ignored startBackgroundHandler - appIsBackground=%d force=%d", appIsBackground, force);
}
}
All NSURLConnections have a runloop - they are started as follows:
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:mrequest delegate:self startImmediately:NO];
if(connection) {
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
} else {
// error handling...
}
PS:
In an earlier version of the app, is used the data fetch background mode instead of voip, and never ran into that kind of problem.
Seems like iOS is not willing to give you CPU time on the main thread while you are in the background - even if you got the VoIP flag. Therfore, you should schedule your request in a separate thread, and use CFRunLoopRun() to make it run in the background. Additionally, you have to trigger execution the appropriate mode, using runMode:beforeDate: on the runloop.
BUT the real problem is that iOS will stop giving CPU time if the requests got timeouts too often - you really need to receive anything in order to get CPU time. Therefore, it is important to have the WebServer replying in time.
Related
I have this code who send data and receive even when the application is in background mode (minimized app):
MyViewController.m
-(void)viewDidAppear:(BOOL)animated{
[self doUpdateEvenAppMinimized];
}
- (void) doUpdate{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundUpdateTask];
[self sendFilesToServer];//Inside this method have a sleep that call every 5 minutes
//The code used in sendFilesToServer is the same in this website https://dcraziee.wordpress.com/2013/05/29/how-to-upload-file-on-server-in-objective-c/
//[self endBackgroundUpdateTask];//This method is forever...so I not need to call this line
});
}
- (void)beginBackgroundUpdateTask{
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}
- (void) endBackgroundUpdateTask{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
The documentation says that the maximum time is 10 minutes, and to remove it I use the concept of Implementing Long-Running Tasks, For that I select my project > capabilities > Background Modes (Turn On) > External accessory communication (Checked).
With these steps, my application will be exempt from the 10 minutes?
Trying to circumvent the rules to run in the background sounds like the wrong approach. Consider using NSURLSession for long running networking operations that are not tied to the lifetime of your app.
I am trying to get this to work correctly but it always seems to end early at 174 seconds (2.9 mins). I been following every tutorial possible online on how to use beginBackgroundTaskWithExpirationHandler and I don't see anything wrong with my code. I need this to end at 8.5 mins. The endBackgroundTask method doesn't even gets call before the expiration handler gets called. Is anything wrong with this?
- (void)applicationDidEnterBackground:(UIApplication *)application {
if([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)])
{
NSLog(#"Multitasking Supported");
backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^ {
NSLog(#"Terminated");
//Clean up code. Tell the system that we are done.
[application endBackgroundTask: backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
}];
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(count:)
userInfo:nil
repeats:YES];
}
else
{
NSLog(#"Multitasking Not Supported");
}
}
-(void)turnOffDesktops:(NSTimer *)time {
//do smth
if(count < (60*8.5)){
NSLog(#"%d",count);
count++;
}else{
[application endBackgroundTask: backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
[timer invalidate];
timer = nil;
count = 0;
}
}
There has never been a commitment from Apple on how long you would be allowed to perform your background tasks. Historically (until iOS7), apps were given usually 10 minutes to run in the background. This is no longer the case! Watch the WWDC 2013 video on backgrounding. With the addition of the new download & upload API in NSURLSession (ability to schedule download and upload tasks on an external dedicated daemon), Apple has reduced the allowed background time significantly. They've done this because this API has always been meant for download and upload, not arbitrary tasks in the background.
You can determine the amount of time left in background by querying - [UIApplication backgroundTimeRemaining]. You can use this to schedule your code to start at the latest possible.
I notice in my app that when application enter background while loading causes the error such as "timeout" or "host name not found" .
It is due to the process that does not allow connection to run in background for a long time.
But that kind of error message make it bad for user experience. So what should I do to cancel the transaction ? Should I just cancel all the connection ? I tried to search the Q&A in SO here but can't find an answer.
For more information, my app use NSURLConnectionDelegate Method. I have a store singleton that manage all connection to my server. NSURLConnection is called and managed in custom object also.
I tried to just [connection cancel] in - applicationDidEnterBackground: but that make the UI broken because I load data to put into UITableViewCell ,etc. Can anyone point to the example to solve this kind of problem?
Updated Code:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
__block UIBackgroundTaskIdentifier backgroundTask; backgroundTask =
[application beginBackgroundTaskWithExpirationHandler: ^ {
[application endBackgroundTask:backgroundTask];
backgroundTask = UIBackgroundTaskInvalid; }]; }
}
Can I just put this code in the appDelegate ? What is the drawback for just doing this versus put the beginBackgroundTaskWithExpirationHandler before the task that I want to keep running in background and endBackgroundTask after that task finished ? My code has one object that deal directly to NSURLConnection.
You are allowed to keep running an NSURLConnection for some period of time after you go into the background. Apple doesn't publish the exact period of time, but it's 10 minutes. See Executing a Finite-Length Task in the Background for details on how to use beginBackgroundTaskWithExpirationHandler: to request more time to complete your download.
In most cases you shouldn't proactively cancel your download. You should wait until the system expires you and then deal with the error at that point. If your download is brief, there's no reason to cancel it (in most cases, the most expensive thing about a connection is setting it up in the first place). If it's a very long download, then the user is going to be annoyed if it doesn't proceed in the background. (This assumes that you're downloading things because the user requested it.)
First, it's better to have this call on the application delegate, so that the View in the NavigationController can be closed. Second, mark beginning of the background processing with beginBackgroundTaskWithExpirationHandler: and end it with endBackgroundTask: like this:
.h:
UIBackgroundTaskIdentifier bgTask;
.m:
- (void)sendPhoto:(UIImage *)image
{
UIApplication *app = [UIApplication sharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
NSLog(#"Sending picture...");
// Init async NSURLConnection
// ....
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"Picture sent.");
UIApplication *app = [UIApplication sharedApplication];
if (bgTask != UIBackgroundTaskInvalid) {
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
}
Also remember one thing its important:
You have 10 minutes before iOS terminates your app. You can check this time with [app backgroundTimeRemaining]
I am using a serial dispatch queue to serialize some network requests when the user moves the app to the background.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
dispatch_queue_t opQ = dispatch_queue_create("com.myapp.network", NULL);
dispatch_async(opQ, ^{
[self sendNetworkData1];
[self sendNetworkData2];
[self sendNetworkData3];
});
}
The problem is that when they run on this queue I have created, the app doesn't stay active even for the 5 seconds it is supposed to.
On the contrary, when I send the same requests outside of a queue, they are being sent for approximately 8 sec. but the app crashes afterwards.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[self sendNetworkData1];
[self sendNetworkData2];
[self sendNetworkData3];
}
I would also like to write the remaining ones on the disk, so that they can be sent the next time the user opens the app.
What's the best way to implement this?
When the application enters the background if it requires additional time to complete some task you will want to notify the OS of that. The detailed documentation is here: http://developer.apple.com/library/ios/#DOCUMENTATION/iPhone/Conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html. Here's a quick and dirty patch.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
__block UIBackgroundTaskIdentifier backgroundTask; //Create a task object
backgroundTask = [application beginBackgroundTaskWithExpirationHandler: ^ {
[application endBackgroundTask:background_task];
backgroundTask = UIBackgroundTaskInvalid; //Set the task to be invalid
}];
dispatch_queue_t opQ = dispatch_queue_create("com.myapp.network", NULL);
dispatch_async(opQ, ^{
[self sendNetworkData1];
[self sendNetworkData2];
[self sendNetworkData3];
[application endBackgroundTask:background_task];
backgroundTask = UIBackgroundTaskInvalid; //Set the task to be invalid
});
}
The bottom line is that you notify that the application needs to run in the background with beginBackgroundTaskWithExpirationHandler: THen when your done you call endBackgroundTask: to notify the OS that you are finished processing in the background. And finally make sure that you reset the backgroundTask variable to UIBackgroundTaskInvalid.
I'm currently writing an application which depends on location tracking and sending data about the position to the server. The problem, however, is that it has to run 24/7 and currently I'm experiencing random crashes which occur every 2-3 days. What I have done to make the application run constantly in the background is I put a NSTimer in a beginBackgroundTaskWithExpirationHandler method right iside the applicationDidEnterBackground method. The timer executes each minute and stops/starts the location service.
Here is a sample crash log
The code basically looks like this:
UIApplication *app = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTaskId = 0;
bgTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval: 1 * 60.0 target: self selector: #selector(onTick) userInfo: nil repeats: YES];
[t fire];
if (bgTaskId != UIBackgroundTaskInvalid){
[app endBackgroundTask: bgTaskId];
bgTaskId = UIBackgroundTaskInvalid;
}
}];
I am using GCDAsyncSockets for connection purposes, each call having a timeout of approximately 30 seconds.
I'm really out of ideas, what might be the reason the crashes occur?
Your timer is probably firing off AFTER the task is invalidated (after [UIApplication sharedApplication].backgroundTimeRemaining gets to 0.
The thing is that you can't make the application run constantly in the background. If you want to execute code every once in a while, your only option is going to be using the background location API, setting that your app is using the location background mode in its plist.
You would be getting the CLLocationManagerDelegate callbacks, and you have some time to do some work when those methods are called.
See the Apple documentation regarding background modes: http://developer.apple.com/library/ios/#DOCUMENTATION/iPhone/Conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
And the location-awarness manual: http://developer.apple.com/library/ios/#DOCUMENTATION/UserExperience/Conceptual/LocationAwarenessPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009497