NSTimer stops firing in Background after some time

2019-03-30 09:18发布

问题:

Hey I am developing an app in which i have to make API call every 30 sec, so i created NSTimer for it. But when my app goes into background timer stops firing after 3-4 minutes. So it works only 3-4 minutes in background,but not after that. How can i modify my code so that timer would not stop.

Here is some of my code.

- (IBAction)didTapStart:(id)sender {
    NSLog(@"hey i m in the timer ..%@",[NSDate date]);
    [objTimer invalidate];
    objTimer=nil;

    UIBackgroundTaskIdentifier bgTask = UIBackgroundTaskInvalid;
    UIApplication  *app = [UIApplication sharedApplication];
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
    }];
    objTimer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:self
                                                       selector:@selector(methodFromTimer) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:objTimer forMode:UITrackingRunLoopMode];
}

-(void)methodFromTimer{
    [LOG debug:@"ViewController.m ::methodFromTimer " Message:[NSString stringWithFormat:@"hey i m from timer ....%@",[NSDate date] ]];
    NSLog(@"hey i m from timer ....%@",[NSDate date]);
}

I even changed the code with the following:

[[NSRunLoop mainRunLoop] addTimer:objTimer forMode:NSRunLoopCommonModes];

This didn't work either.

回答1:

Don't create UIBackgroundTaskIdentifier task as local and make it global as below:

Step -1

Step -2

Step -3

Step -4

As local one loose scope and global one won't ,and I created a demo and ran it for sometime with 1 sec repeating timer ,and worked smooth. Still if u face issue pls let me know.

I ran again demo and here are logs of it running.

So its working fine and more than 3 minutes. Also that 3 minute logic is right but as uibackgroundtask is initiated so it shouldn't let it kill this task of timer.

Edited Part:- bgTask = [app beginBackgroundTaskWithExpirationHandler:^{ [app endBackgroundTask:bgTask]; //Remove this line and it will run as long as timer is running and when app is killed then automatically all vairbles and scopes of it are dumped. }];

Check it and let me know if it works out or not.

Hey I run ur code and I reached the expirationHandler but after released debug point ,the timer was running smooth.



回答2:

No, don't do background tasks with NSTimer. It will not work as you might expect. You should be making use of background fetch APIs provided by Apple only. You can set the duration at which you want it to be called in that API. Though usually it is not recommended setting duration of the call you would like to make. Take a look at this apple background programming documentation

Also, to get you started quickly, you can follow this Appcoda tutorial



回答3:

It is normal behavior.

After iOS7, you got exactly 3 minutes of background time. Before that there was 10 minutes if i remember correctly. To extend that, your app needs to use some special services like location, audio or bluetooth which will keep it "alive" in the background.

Also, even if you use one of these services the "Background app refresh" setting must be enabled on your device for the app.

See this answer for details or the background execution part of the documentatio.



回答4:

This worked for me, so I'm adding it to StackOverflow for any future answer seekers.

Add the following utility method to be called before you start your timer. When we call AppDelegate.RestartBackgroundTimer() it will ensure that your app will remain active - even if it's in the background or if the screen is locked. However, this will only ensure that for 3 minutes (as you mentioned):

class AppDelegate: UIResponder, UIApplicationDelegate {
    static var backgroundTaskIdentifier: UIBackgroundTaskIdentifier? = nil;

    static func RestartBackgroundTimer() {
        if (AppDelegate.backgroundTaskIdentifier != nil) {
            print("RestartBackgroundTimer: Ended existing background task");
            UIApplication.sharedApplication().endBackgroundTask(AppDelegate.backgroundTaskIdentifier!);
            AppDelegate.backgroundTaskIdentifier = nil;
        }
        print("RestartBackgroundTimer: Started new background task");
        AppDelegate.backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
            UIApplication.sharedApplication().endBackgroundTask(AppDelegate.backgroundTaskIdentifier!);
            AppDelegate.backgroundTaskIdentifier = nil;
        })
    }
}

Also, when starting your app, ensure the following runs. It will ensure that audio is played even if the app is in the background (and while you're at it, also ensure that your Info.plist contains "Required background modes" and that "App plays audio or streams audio/video using AirPlay" a.k.a. "audio" is in its collection):

import AVFoundation;

// setup audio to not stop in background or when silent
do {
    try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback);
    try AVAudioSession.sharedInstance().setActive(true);
} catch { }

Now, in the class that needs the timer to run more than 3 minutes (if in the background), you need to play a sound when only 30 seconds remains of background time. This will reset the background time remaining to 3 minutes (just create a "Silent.mp3" with e.g. AudaCity and drag & drop it to your XCode project).

To wrap it all up, do something like this:

import AVFoundation

class MyViewController : UIViewController {
    var timer : NSTimer!;

    override func viewDidLoad() {
        // ensure we get background time & start timer
        AppDelegate.RestartBackgroundTimer();
        self.timer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: #selector(MyViewController.timerInterval), userInfo: nil, repeats: true);
    }

    func timerInterval() {
        var bgTimeRemaining = UIApplication.sharedApplication().backgroundTimeRemaining;
        print("Timer... " + NSDateComponentsFormatter().stringFromTimeInterval(bgTimeRemaining)!);
        if NSInteger(bgTimeRemaining) < 30 {
            // 30 seconds of background time remaining, play silent sound!
            do {
                var audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Silent", ofType: "mp3")!));
                audioPlayer.prepareToPlay();
                audioPlayer.play();
            } catch { }
        }
    }
}