Is NSTimer expected to fire when app is backgrounded?
Asked Answered
K

2

18

I don't understand it at all but NSTimer in my app definitely is running in background. I have a NSLog in method run by the timer and it is logging while it's in background. It's on iPhone 4 with iOS 4.2.1. I have declared location background support in Info.plist.

I read the docs and many discussions here and elsewhere and it shouldn't be possible. Is it an iOS bug? Or undocumented feature? I don't want to use it and find out in near future, for example with coming of iOS 4.3 that Apple silently "fixed" it and the app won't be working.

Does anybody know more about it?

Kragh answered 3/3, 2011 at 23:4 Comment(0)
I
33

NSTimer is going to fire whenever the main runloop is running. Apple makes no promises that I know of to unschedule timers or to prevent the main runloop from running. It's your responsibility to unschedule your timers and release resources when you move to the background. Apple isn't going to do it for you. They may, however, kill you for running when you are not supposed to or using too many seconds.

There are many holes in the system that will allow an app to run when it isn't authorized to. It would be very expensive for the OS to prevent this. But you cannot rely on it.

Inga answered 3/3, 2011 at 23:16 Comment(2)
Ok, that's sufficient answer for me. Thanks. I didn't know if I missed something. I won't use it for sure.Kragh
I'm observing the behavior you're saying. I’m using a timer to refresh a token every 10 mins. If the app is backgrounded for 15 mins and then foregrounded then upon foregrounding, the timer would fire. I didn't expect this. Thought the timer is no longer running in the background and nothing happens. It seems that somehow all non-fired/non-invalidated timers are stored strongly by the runloop and upon foregrounding if the fireDate is passed then they just get fired immediately. So can I safely rely on a fireDate getting passed or I should store a Date and compare against that?Rivy
M
8

You can have a timer fire while in background execution mode. There are a couple of tricks:

  • You need to opt into background execution with beginBackgroundTaskWithExpirationHandler.
  • If you create the NSTimer on a background thread, you need to add it to the mainRunLoop manually.

- (void)viewDidLoad
{
    // Avoid a retain cycle
    __weak ViewController * weakSelf = self;

    // Declare the start of a background task
    // If you do not do this then the mainRunLoop will stop
    // firing when the application enters the background
    self.backgroundTaskIdentifier =
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundIdentifier];
    }];

    // Make sure you end the background task when you no longer need background execution:
    // [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];


    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // Since we are not on the main run loop this will NOT work:
        [NSTimer scheduledTimerWithTimeInterval:0.5
                                         target:self
                                       selector:@selector(timerDidFire:)
                                       userInfo:nil
                                        repeats:YES];

        // This is because the |scheduledTimerWithTimeInterval| uses
        // [NSRunLoop currentRunLoop] which will return a new background run loop
        // which will not be currently running.
        // Instead do this:
        NSTimer * timer =
        [NSTimer timerWithTimeInterval:0.5
                                target:weakSelf
                              selector:@selector(timerDidFire:)
                              userInfo:nil
                               repeats:YES];

        [[NSRunLoop mainRunLoop] addTimer:timer
                                  forMode:NSDefaultRunLoopMode];
        // or use |NSRunLoopCommonModes| if you want the timer to fire while scrolling
    });
}

- (void) timerDidFire:(NSTimer *)timer
{
    // This method might be called when the application is in the background.
    // Ensure you do not do anything that will trigger the GPU (e.g. animations)
    // See: http://developer.apple.com/library/ios/DOCUMENTATION/iPhone/Conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html#//apple_ref/doc/uid/TP40007072-CH4-SW47
    NSLog(@"Timer did fire");
}

Notes

  • Apps only get ~ 10 mins of background execution - after this the timer will stop firing.
  • As of iOS 7 when the device is locked it will suspend the foreground app almost instantly. The timer will not fire after an iOS 7 app is locked.
Marvismarwin answered 3/7, 2013 at 10:32 Comment(5)
Great peace of code, this will help me finish my app, THNX :). One little question, what do you meen by "This method MIGHT be called when the application is in the background."? I want to use this for updating data from server once in the 3 hours. Is there a better approach?Milurd
Your application will only run for a maximum of 10 mins when put into the background. As of iOS 7 these 10 mins can be disjoint. You might be able to use use iOS7's new background modes: background transfer for large upload / download or silent push for waking you app up remotely.Marvismarwin
This code sample suggests a confusion between foreground/background state of the app itself and the main queue vs global queue issue. There is no need to dispatch the scheduling of the timer to a global queue. Just schedule the timer on the main queue, like normal, and the beginBackgroundTaskWithExpirationHandler will keep the app alive for a few minutes.Stew
@Stew I'm getting it to fire for 180 seconds without having to wrap it in beginBackgroundTaskWithExpirationHandler...Any reason you're suggesting to use beginBackgroundTaskWithExpirationHandler?Rivy
You’re probably doing this when you run the app attached to the Xcode debugger, which changes the app lifecycle. Run it while not attached to Xcode (e.g. quit Xcode and run directly from the device) and you’ll find the app is suspended much more quickly (unless you do this background task stuff).Stew

© 2022 - 2024 — McMap. All rights reserved.