Android中如何让CountDownTimer长时间稳定运行?
问题描述
我正在开发一款支持用户自定义时长的倒计时计时器,计时器归零后会播放提示音。但在长计时场景下遇到了问题:提示音不是在计时器归零的瞬间播放,而是要等解锁手机时才响起,同时应用会重置回到主界面。使用前台服务时,计时器最多能正常运行约30分钟;不使用前台服务时,最长运行时间甚至不足10分钟。请问该采用什么方案才能让计时器持续正常工作?
相关代码片段
TimerActivity 类
// TimerActivity class override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityTimerBinding.inflate(layoutInflater) setContentView(binding.root) textViewTimer = binding.textViewTimer buttonPause = binding.buttonPause buttonStop = binding.buttonStop mediaPlayer = MediaPlayer.create(this, R.raw.timersound) mediaPlayer.isLooping = false buttonPause.setOnClickListener { if (!isTimerPaused) { // Pause the countdown timer countdownTimer.cancel() isTimerPaused = true buttonPause.text = getString(R.string.resume) Intent(applicationContext, TimerService::class.java).also { it.action = TimerService.PAUSE startService(it) } } else { // Resume the countdown timer startCountdownTimer(timeRemainingMillis) isTimerPaused = false buttonPause.text = getString(R.string.pause) } } buttonStop.setOnClickListener { finishAndReturnActivity(timeRemainingMillis) } val timerMinutes = intent.getLongExtra("timerMinutes", 1) startCountdownTimer(timerMinutes * 60 * 1000) } private fun createCountDownTimer(millis: Long): CountDownTimer { // Create and return a new CountDownTimer return object : CountDownTimer(millis, 1000) { override fun onTick(millisUntilFinished: Long) { // Update the countdown display on each tick timeRemainingMillis = millisUntilFinished val minutes = (millisUntilFinished / 1000) / 60 val seconds = (millisUntilFinished / 1000) % 60 val formattedTime = String.format("%02d:%02d", minutes, seconds) textViewTimer.text = formattedTime } override fun onFinish() { textViewTimer.text = "00:00" playBellSound() val timeLeft: Long = 0 finishAndReturnActivity(timeLeft) } } } private fun startCountdownTimer(millis: Long) { Intent(applicationContext, TimerService::class.java).also { it.action = TimerService.START startService(it) } // Initialize the countdown timer with an initial time of timerMinutes minutes countdownTimer = createCountDownTimer(millis) countdownTimer.start() }
TimerService 类
// TimerService class override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { START -> start() PAUSE -> stopSelf() } return super.onStartCommand(intent, flags, startId) } private fun start() { val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Timer is running") .setContentText("Your timer is active.") .build() startForeground(1, notification) }
解决方案
1. 将计时核心逻辑移至前台服务
当前代码的前台服务仅作为“占位”通知,实际计时逻辑在Activity中。Activity进入后台后极易被系统回收,导致计时中断、提示音延迟。需将CountDownTimer的逻辑完全迁移至Service:
- 服务内部处理计时的开始、暂停、恢复,确保即使Activity销毁,服务仍能持续运行。
- 通过广播或LiveData向Activity同步计时状态,更新界面显示。
2. 超长时间计时用AlarmManager替代CountDownTimer
CountDownTimer依赖Handler,进程被回收后会直接失效。对于超过30分钟的长计时,推荐用AlarmManager设置精确闹钟:
- 计算计时结束的目标时间,使用
setExactAndAllowWhileIdle()方法,确保在Doze模式下也能触发。 - 闹钟触发时启动服务或广播,执行提示音播放逻辑。
3. 优化前台服务稳定性
- 在
onStartCommand返回START_REDELIVER_INTENT,确保服务被系统杀死后能自动重启,并传递之前的计时状态。 - 为前台服务设置合法的通知渠道(Android 8.0+强制要求),并保持通知内容实时更新(如显示剩余时间)。
4. 处理进程被杀后的恢复逻辑
- Activity启动时,检查前台服务是否在运行,若运行则同步当前剩余计时,避免界面重置。
- 将剩余计时时间保存到SharedPreferences或本地数据库,服务重启时读取并恢复计时进度。
5. 提示音播放逻辑迁移至服务
当前提示音在Activity的onFinish中播放,若Activity已被销毁,需等解锁重建后才会触发。将播放逻辑移至服务中,确保计时结束时立即执行:
- 在服务中初始化
MediaPlayer,计时结束直接调用start()播放。 - 申请音频焦点,避免被其他应用音频打断。
优化后代码示例
修改后的TimerService
class TimerService : Service() { companion object { const val START = "START" const val PAUSE = "PAUSE" const val RESUME = "RESUME" const val ACTION_UPDATE_TIMER = "ACTION_UPDATE_TIMER" const val EXTRA_TIME_REMAINING = "EXTRA_TIME_REMAINING" private const val CHANNEL_ID = "TIMER_CHANNEL" } private var countdownTimer: CountDownTimer? = null private var timeRemainingMillis: Long = 0 private lateinit var mediaPlayer: MediaPlayer override fun onCreate() { super.onCreate() mediaPlayer = MediaPlayer.create(this, R.raw.timersound) mediaPlayer.isLooping = false createNotificationChannel() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { START -> { timeRemainingMillis = intent.getLongExtra(EXTRA_TIME_REMAINING, 0) startForegroundTimer() } PAUSE -> { countdownTimer?.cancel() stopForeground(false) // 暂停时保留通知,可根据需求调整 } RESUME -> startForegroundTimer() } return START_REDELIVER_INTENT // 服务重启时恢复状态 } private fun startForegroundTimer() { updateNotification() startForeground(1, buildNotification()) countdownTimer?.cancel() countdownTimer = object : CountDownTimer(timeRemainingMillis, 1000) { override fun onTick(millisUntilFinished: Long) { timeRemainingMillis = millisUntilFinished // 发送广播更新Activity界面 sendBroadcast(Intent(ACTION_UPDATE_TIMER).putExtra(EXTRA_TIME_REMAINING, millisUntilFinished)) updateNotification() } override fun onFinish() { playBellSound() sendBroadcast(Intent(ACTION_UPDATE_TIMER).putExtra(EXTRA_TIME_REMAINING, 0L)) stopForeground(true) stopSelf() } }.start() } private fun buildNotification(): Notification { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("计时器运行中") .setContentText("剩余时间:${formatTime(timeRemainingMillis)}") .setSmallIcon(R.drawable.ic_timer) .build() } private fun updateNotification() { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(1, buildNotification()) } private fun formatTime(millis: Long): String { val minutes = (millis / 1000) / 60 val seconds = (millis / 1000) % 60 return String.format("%02d:%02d", minutes, seconds) } private fun playBellSound() { if (mediaPlayer.isPlaying) { mediaPlayer.stop() mediaPlayer.reset() } mediaPlayer.start() } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(CHANNEL_ID, "计时器通知", NotificationManager.IMPORTANCE_LOW) val manager = getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } override fun onDestroy() { super.onDestroy() countdownTimer?.cancel() mediaPlayer.release() } override fun onBind(intent: Intent): IBinder? = null }
修改后的TimerActivity
class TimerActivity : AppCompatActivity() { private lateinit var binding: ActivityTimerBinding private var isTimerPaused = false private var timeRemainingMillis: Long = 0 private val timerUpdateReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { intent?.getLongExtra(TimerService.EXTRA_TIME_REMAINING, 0)?.let { millis -> timeRemainingMillis = millis binding.textViewTimer.text = String.format("%02d:%02d", (millis/1000)/60, (millis/1000)%60) if (millis == 0L) finishAndReturnActivity(0L) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityTimerBinding.inflate(layoutInflater) setContentView(binding.root) binding.buttonPause.setOnClickListener { isTimerPaused = !isTimerPaused binding.buttonPause.text = if (isTimerPaused) getString(R.string.resume) else getString(R.string.pause) Intent(this, TimerService::class.java).apply { action = if (isTimerPaused) TimerService.PAUSE else TimerService.RESUME startService(this) } } binding.buttonStop.setOnClickListener { Intent(this, TimerService::class.java).apply { action = TimerService.PAUSE startService(this) } finishAndReturnActivity(timeRemainingMillis) } val timerMinutes = intent.getLongExtra("timerMinutes", 1) timeRemainingMillis = timerMinutes * 60 * 1000 Intent(this, TimerService::class.java).apply { action = TimerService.START putExtra(TimerService.EXTRA_TIME_REMAINING, timeRemainingMillis) startService(this) } } override fun onResume() { super.onResume() registerReceiver(timerUpdateReceiver, IntentFilter(TimerService.ACTION_UPDATE_TIMER)) } override fun onPause() { super.onPause() unregisterReceiver(timerUpdateReceiver) } private fun finishAndReturnActivity(timeLeft: Long) { // 实现返回主界面的逻辑 finish() } }
内容的提问来源于stack exchange,提问作者Roihu
相关产品推荐
相关产品推荐

