You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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,提问作者Мухаммаднакшубанди Омаров

火山引擎 最新活动