3

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.

Z S
  • 7,039
  • 12
  • 53
  • 105

1 Answers1

3

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thanks. The timer should call `endBackgroundTask` on any existing identifier and mark it invalid right at the start of the timer, before it begins another task, so would that still be a problem? Looking at it again, it looks to me the problem might be that the 'backup' process might take more than 60 seconds, so by the time it's done and the completion block is called, it's calling `endBackgroundTask` on an expired identifier? Would that generate the same problem and stack trace, or do you think it's still a problem with some identifiers that don't get an `endBackgroundTask`? – Z S Nov 09 '15 at 17:13
  • I think we're saying the same thing. Look, I can't figure out what your code does, and I don't think you can either. But we do know you've got just one identifier and yet you have the possibility of multiple simultaneous backup tasks. That's nuts, even if that _never_ happens (and it sounds like in your testing it didn't). Hence my suggestion: each backup is its own NSOperation with its own background task and its own background task identifier. No more "loop"! Your code will be way simpler. – matt Nov 09 '15 at 17:59
  • I agree with the general point, and I think I see the problem. The fix would be to take out the taskIdentifier from the NSTimer function, and only call it to wrap around the actual backup functionality (also add a boolean to prevent calling the backup code while backup is running). That way, there's only one background task going on at a time. – Z S Nov 10 '15 at 03:46
  • Yes, that's just what I've been saying. One identifier, one task, one backup. But they need not be sequential. Once you have this structured as an NSOperation, they can be somewhat concurrent if needed. – matt Nov 10 '15 at 03:47