iOS锁屏时NSTimer停止运行,如何实现锁屏后台持续计时?
嘿,这个问题我太熟了!NSTimer在锁屏后停摆,本质是因为iOS App进入后台后,主线程的RunLoop会进入休眠状态,而NSTimer完全依赖RunLoop来触发事件,自然就歇菜了。下面给你几个靠谱的解决办法,结合你的代码来改:
方案一:利用后台任务延长后台运行时间
这个适合短时间的后台任务(比如你的5分钟定时器刚好在系统允许的后台时长范围内),核心是让系统给App多留一点后台运行时间:
首先在你的ViewController类里添加一个后台任务标识符的属性:
@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskID;
然后修改你的启动定时器和计时方法:
-(IBAction)Start:(id)sender{ secondsCount = perVrem; // 开启后台任务,请求系统延长后台运行时间 self.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"CountdownTimer" expirationHandler:^{ // 如果系统即将终止后台任务,主动结束它避免被标记为违规 [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskID]; self.backgroundTaskID = UIBackgroundTaskInvalid; }]; countdownTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeRun) userInfo:nil repeats:YES]; } -(void)timeRun{ secondsCount = secondsCount - 1; int minuts = secondsCount / 60; int seconds = secondsCount - (minuts * 60); NSString *timerOutput = [NSString stringWithFormat:@"%2d:%.2d", minuts, seconds]; TimerDisplay.text = timerOutput; if (secondsCount == 0) { [countdownTimer invalidate]; countdownTimer = nil; AudioServicesPlaySystemSound(PlaySoundID); // 定时器结束,主动结束后台任务 if (self.backgroundTaskID != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskID]; self.backgroundTaskID = UIBackgroundTaskInvalid; } } }
另外记得在AppDelegate里监听App回到前台的事件,及时结束后台任务:
- (void)applicationWillEnterForeground:(UIApplication *)application { // 假设你的ViewController是rootViewController,根据实际情况调整 YourViewController *vc = (YourViewController *)self.window.rootViewController; if (vc.backgroundTaskID != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:vc.backgroundTaskID]; vc.backgroundTaskID = UIBackgroundTaskInvalid; } }
方案二:改用GCD定时器(更可靠)
GCD定时器不依赖主线程RunLoop,在后台也能稳定触发,是更推荐的方案。修改你的代码如下:
首先声明GCD定时器变量:
@property (nonatomic, strong) dispatch_source_t countdownTimer;
然后重写启动定时器的方法:
-(IBAction)Start:(id)sender{ secondsCount = perVrem; // 先销毁之前的定时器,避免重复创建 if (self.countdownTimer) { dispatch_source_cancel(self.countdownTimer); self.countdownTimer = nil; } // 创建GCD定时器,用全局队列避免阻塞主线程 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); self.countdownTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 设置定时器参数:立即启动、每秒触发一次、允许0.1秒的误差 dispatch_source_set_timer(self.countdownTimer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); // 设置定时回调 dispatch_source_set_event_handler(self.countdownTimer, ^{ // 更新UI必须回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ secondsCount = secondsCount - 1; int minuts = secondsCount / 60; int seconds = secondsCount - (minuts * 60); NSString *timerOutput = [NSString stringWithFormat:@"%2d:%.2d", minuts, seconds]; TimerDisplay.text = timerOutput; if (secondsCount == 0) { dispatch_source_cancel(self.countdownTimer); self.countdownTimer = nil; AudioServicesPlaySystemSound(PlaySoundID); } }); }); // 启动定时器 dispatch_resume(self.countdownTimer); }
这个方案不需要额外申请后台任务,只要App没被系统主动杀死,定时器就能在后台正常运行,而且精度比NSTimer更高。
方案三:监听系统时间变化(适合长时间后台)
如果你的定时器需要运行更长时间,系统可能会强制终止后台任务,这时候可以通过监听系统时间变化,计算锁屏前后的时间差来同步计时状态:
在viewDidLoad里添加通知监听:
@property (nonatomic, strong) NSDate *lastUpdateDate; - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTimeChange:) name:UIApplicationSignificantTimeChangeNotification object:nil]; self.lastUpdateDate = [NSDate date]; } - (void)handleTimeChange:(NSNotification *)notification { // 计算时间差,更新剩余秒数 NSTimeInterval timeDiff = [[NSDate date] timeIntervalSinceDate:self.lastUpdateDate]; secondsCount -= timeDiff; if (secondsCount < 0) secondsCount = 0; // 更新UI int minuts = secondsCount / 60; int seconds = secondsCount - (minuts * 60); NSString *timerOutput = [NSString stringWithFormat:@"%2d:%.2d", minuts, seconds]; TimerDisplay.text = timerOutput; if (secondsCount == 0) { // 停止定时器并播放提示音 if (self.countdownTimer) { dispatch_source_cancel(self.countdownTimer); self.countdownTimer = nil; } AudioServicesPlaySystemSound(PlaySoundID); } // 更新上次记录的时间 self.lastUpdateDate = [NSDate date]; }
这个方案适合不需要每秒精准触发的场景,即使App被系统挂起,也能在恢复后快速同步计时状态。
内容的提问来源于stack exchange,提问作者Мухаммаднакшубанди Омаров




