如何在Android平台实现具备实时性保障的秒表功能?
前台服务实现高精度秒表的规范方案
原有方案的核心缺陷是依赖
handler.postDelayed()的执行间隔做累加计数,系统进入休眠状态时主线程消息队列调度会被冻结,自然会出现计时偏差甚至完全停止的问题。
核心实现逻辑
- 完全取消每秒累加的计时逻辑,改为记录关键时间节点的时间戳,每次需要获取当前计时值时通过时间差计算,从根源避免调度偏差导致的误差
- 每秒的UI刷新广播仅做通知用,不参与计时逻辑,哪怕广播延迟也不会影响最终计时结果
- 仅需要存储3个核心变量即可支持启动、暂停、恢复、停止全操作:
- 记录计时累计时长(暂停时存储已经完成的计时时长)
- 记录本次启动/恢复的时间戳
- 计时状态标记(运行/暂停/停止)
改造后的代码实现
class TimerService: Service(), CoroutineScope { // 计时状态枚举 enum class TimerState { STOP, START, PAUSE } // 累计已经走过的时长(单位ms,暂停时存入) private var elapsedTime: Long = 0L // 本次启动/恢复时的系统时间戳(单位ms) private var lastStartTime: Long = 0L // 当前计时状态 private var timerState = TimerState.STOP private val job = Job() override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job // 每秒触发广播的runnable,仅做通知,不参与计时计算 private val handler = Handler(Looper.getMainLooper()) private val refreshRunnable = object : Runnable { override fun run() { broadcastUpdate() handler.postDelayed(this, 1000) } } // 对外暴露的启动方法 fun startTimer() { if (timerState == TimerState.START) return lastStartTime = SystemClock.elapsedRealtime() timerState = TimerState.START handler.post(refreshRunnable) // 原有前台服务启动逻辑保留 } // 对外暴露的暂停方法 fun pauseTimer() { if (timerState != TimerState.START) return // 暂停时先把当前已经跑的时间算出来存到elapsedTime elapsedTime += SystemClock.elapsedRealtime() - lastStartTime timerState = TimerState.PAUSE handler.removeCallbacks(refreshRunnable) broadcastUpdate() } // 对外暴露的恢复方法,直接复用startTimer逻辑即可 fun resumeTimer() = startTimer() // 对外暴露的停止方法 fun stopTimer() { timerState = TimerState.STOP elapsedTime = 0L lastStartTime = 0L handler.removeCallbacks(refreshRunnable) broadcastUpdate() // 原有前台服务销毁逻辑保留 } // 每次调用实时计算当前总计时秒数,无累计误差 private fun getCurrentTotalSeconds(): Int { return if (timerState == TimerState.START) { val current = elapsedTime + (SystemClock.elapsedRealtime() - lastStartTime) (current / 1000).toInt() } else { (elapsedTime / 1000).toInt() } } private fun broadcastUpdate() { val currentTime = getCurrentTotalSeconds() // 原有广播发送逻辑,直接把currentTime发出去即可 ... } // 其余服务原有逻辑保留 ... }
注意事项
- 必须用
SystemClock.elapsedRealtime()获取时间,不要用System.currentTimeMillis(),后者会跟随系统时间调整,修改系统时间会导致计时出现跳变 - 休眠状态下如果觉得1秒刷新延迟太高,可以给前台服务加唤醒锁(WakeLock),但注意不需要一直持有,仅做刷新用的话不会太耗电。一般场景下不加也不影响计时准确性,只是UI刷新可能延迟,亮屏后会自动校正到正确时间
- 前台服务必须按照安卓要求展示常驻通知,否则安卓12及以上系统会自动回收服务
内容的提问来源于stack exchange,提问作者Cameron
相关产品推荐
相关产品推荐

