iosobjective-cnstimeruiapplicationuibackgroundtask

iOS app "has active assertions beyond permitted time - occasional crashes"


Some of my users are getting this crash (according to them, it happens after 4-5 minutes of using the app) but I can't reproduce it myself:

Application Specific Information:
<BKNewProcess: 0x175466c0; com.zsquare.iPadApp; pid: 5005; hostpid: -1> has active assertions beyond permitted time: 
{(
    <BKProcessAssertion: 0x17545c90> id: 48-3A424578-FF1D-4484-9026-B4C6A83AD7EF name: Background Content Fetching (191) process: <BKNewProcess: 0x175466c0; .com.zsquare.ijournalPad; pid: 5005; hostpid: -1> permittedBackgroundDuration: 30.000000 reason: backgroundContentFetching owner pid:48 preventSuspend  preventThrottleDownUI  preventIdleSleep  preventSuspendOnSleep 
)}

Elapsed total CPU time (seconds): 0.460 (user 0.460, system 0.000), 2% CPU 
Elapsed application CPU time (seconds): 0.013, 0% CPU

Filtered syslog: None found

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0:
0   libsystem_kernel.dylib          0x362a4ff0 mach_msg_trap + 20
1   libsystem_kernel.dylib          0x362a4df4 mach_msg + 38
2   CoreFoundation                  0x23fa58c4 __CFRunLoopServiceMachPort + 134
3   CoreFoundation                  0x23fa3c4c __CFRunLoopRun + 1034
4   CoreFoundation                  0x23ef7118 CFRunLoopRunSpecific + 518
5   CoreFoundation                  0x23ef6f04 CFRunLoopRunInMode + 106
6   GraphicsServices                0x2d081ac8 GSEventRunModal + 158
7   UIKit                           0x28139f14 UIApplicationMain + 142
8   SimpleList-iPad                 0x0000f116 main (main.m:17)
9   libdyld.dylib                   0x361e9872 start + 0

Now I've looked at various other SO questions that deal with this crash, but none of the answers there have been helpful to me, so I thought I'd post here with my own setup and code.

First, the functionality where this happens is related to a repeated task that should run in the app. For this, I have a timer repeating in the app when the app first starts:

- (void) setupRepeatTimer {

self.repeatTimer = [NSTimer scheduledTimerWithTimeInterval: 60.0 target:self selector:@selector(checkForAutomaticTaskTime:) userInfo:nil repeats: YES];
self.repeatTimer.tolerance = 1.0;

}

- (void) checkForAutomaticTaskTime: (NSTimer *) timer {

   if (self.taskIdentifier) {
    [[UIApplication sharedApplication] endBackgroundTask: self.taskIdentifier];
   }

   self.taskIdentifier = UIBackgroundTaskInvalid;
   [self beginBackgroundUpdateTask];


   // simplified, but there are more conditional checks here
   if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive && setting_enabled_for_automatic_task) {
        [self startAutomaticBackup];
   } else {
        [self endBackgroundUpdateTask];
   }
}

The backup that needs to be run can be variable sized, depending on the user's data. Here's the general code for this:

- (void) startAutomaticBackup {

    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        [self runBackupWithCompletionBlock:^(NSString * filename) {

            dispatch_async(dispatch_get_main_queue(), ^{
                // finish on the main thread
                [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

                [self endBackgroundUpdateTask];

            });
        }];
    });
}

Just to be sure, the beginBackgroundUpdateTask and endBackgroundUpdateTask look exactly like those from code samples in SO and Apple's guide:

- (void) beginBackgroundUpdateTask
{
    self.taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"CJAutoBackup" expirationHandler:^{
        NSLog(@"beginBackgroundTask about to end = %lu", (unsigned long)self.taskIdentifier);
        [self endBackgroundUpdateTask];
    }];
}

- (void) endBackgroundUpdateTask
{
    if (self.taskIdentifier) {
        [[UIApplication sharedApplication] endBackgroundTask: self.taskIdentifier];
        self.taskIdentifier = UIBackgroundTaskInvalid;
    }
}

====

So that's the setup I have. From the user descriptions, it seems the app crashes when in the foreground with this crash report, after the app has been running for 4-5 minutes. If they disable the setting for automatic backup, the app works fine again.

The main confusion here is that the app crashes when the app uses it in the foreground, not the background, but the error message talks about background task assertions. How is that possible?

Also, what could I be doing to prevent this problem? I've seen UIApplication's backgroundTimeRemaining property mentioned, but I'm not sure how or where to use it in my example.

Does using the NSTimer cause any complication? On device, it doesn't seem to trigger the timer if the app is in the background already.

Appreciate the help.


Solution

  • It's a threading issue. The problem is indeed, as you suspect, the factoring and architecture of your begin/end background task calls. You are attempting to maintain one background task identifier as a property, which you are then repeatedly changing every 60 seconds, regardless of how long the actual task takes (on a background thread). This confusing pseudo-loop architecture results in your background tasks getting out of sync with their identifiers, and so the time on a background task expires without your calling endBackgroundTask with the appropriate identifier.

    You need to revise this architecture so that you have one background task identifier per task, thus keeping everything separate. I think you'll find the easiest way is to express each automatic backup as a separate NSOperation.