You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:07:07