一、项目介绍
1. 背景与动机
倒计时(Countdown)功能在移动应用中十分常见,主要应用场景包括:
-
验证码倒计时:获取短信验证码后,按钮倒计时重新发送
-
活动倒计时:限时抢购、秒杀、直播等活动开始前的预热
-
游戏/教育:闯关计时、生存模式等
-
退出提示:退出应用前给用户若干秒“再按一次退出”提示
虽然需求相似,但实现方式各不相同:有的基于 Handler
,有的使用 CountDownTimer
,也有人选择 RxJava
、Kotlin Coroutine
、AlarmManager
、Chronometer
,甚至 WorkManager
。不同方案在易用性、准确性、资源消耗、生命周期感知等方面各有优劣。
2. 文章目标
本教程旨在以一篇超长、结构完整的博客形式,从理论到实践深度剖析 Android 实现倒计时的多种方案,包括但不限于:
-
Handler + Runnable
-
CountDownTimer
-
Timer + TimerTask + Handler
-
ScheduledThreadPoolExecutor
-
RxJava2/3 Observable.interval
-
Kotlin Coroutine + Flow
-
Chronometer 控件
-
AlarmManager
-
HandlerThread
-
WorkManager(一秒级倒计时)
-
Jetpack Compose + LaunchedEffect
以上方案的完整代码将整合到一起,不拆分多个文件,使用注释区分。并将深入对比各方案优缺点、性能、生命周期处理、内存泄漏风险,以及最佳实践和优化策略。
二、相关知识
在开始编码之前,先了解以下 Android 基础与高级知识点:
-
Android 线程与消息机制
-
Looper
、Handler
、MessageQueue
-
主线程(UI 线程)与后台线程区别
-
-
Java 并发工具
-
TimerTask
、ScheduledExecutorService
-
CountDownLatch
、ExecutorService
-
-
Android 专用计时类
-
CountDownTimer
:系统封装的倒计时工具 -
Chronometer
:系统提供的计时控件
-
-
响应式编程
-
RxJava:
Observable.interval()
+Schedulers
; -
Kotlin Coroutine:
Flow
,delay()
,ticker()
-
-
系统组件
-
AlarmManager:定时闹钟,适合长时间(分钟级以上)倒计时
-
WorkManager:后台任务管理,用于可靠执行
-
-
Jetpack Compose
-
LaunchedEffect
+snapshotFlow
构建 Compose 界面级倒计时
-
-
生命周期感知
-
防止内存泄漏:关闭、移除回调;在
onDestroy()
或onPause()
中清理
-
三、实现思路
-
功能需求确认
-
短时间倒计时(30s、60s)常用于验证码;
-
长时间倒计时(分钟/小时)用于活动预热;
-
UI 需显示剩余时间,并支持暂停/取消。
-
-
方案对比
方案 精度 简易度 生命周期感知 资源消耗 适用场景 Handler + Runnable 高 中 手动清理 低 简单短倒计时 CountDownTimer 中 高 自动清理 低 常见验证码倒计时 Timer + TimerTask + Handler 中 低 手动清理 中 简单任务调度 ScheduledThreadPoolExecutor 高 中 手动清理 中 多任务并发倒计时 RxJava.interval 高 中 自动清理 中高 响应式项目 Coroutine + Flow 高 高 自动清理 低 Kotlin 项目首选 Chronometer 低 高 自动 低 简单正向计时 AlarmManager 低 低 系统托管 低 后台长时、重启重置 HandlerThread 高 低 手动清理 低 自定义 Looper WorkManager 低 中 系统托管 中 可靠后台倒计时 Compose LaunchedEffect 高 高 自动 低 Compose 界面倒计时 -
代码组织
-
所有方案的 Activity、布局、工具类、依赖统一写在一个“整合代码”区块中,不拆文件,使用注释
// === 文件: xxx ===
区分; -
详细注释每一行关键逻辑;
-
-
清理与防泄漏
-
各方式结束时移除回调、取消订阅、关闭定时器;
-
在
onDestroy()
中统一清理;
-
-
UI 层
-
使用同一布局展示倒计时按钮和文本;
-
不同方案可通过注释切换;
-
四、环境与依赖
// 文件: app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.countdownsamples"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "1.4.5"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// Kotlin & AndroidX
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.0"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
// RxJava
implementation 'io.reactivex.rxjava3:rxjava:3.1.5'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
// Coroutines & Flow
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
// WorkManager
implementation 'androidx.work:work-runtime-ktx:2.8.1'
// Compose
implementation 'androidx.activity:activity-compose:1.7.0'
implementation 'androidx.compose.ui:ui:1.4.5'
implementation 'androidx.compose.material:material:1.4.5'
implementation 'androidx.compose.ui:ui-tooling:1.4.5'
}
五、整合代码
// =======================================================
// 文件: app/src/main/AndroidManifest.xml
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.countdownsamples">
<application
android:allowBackup="true"
android:label="倒计时示例"
android:theme="@style/Theme.CountdownSamples">
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
// =======================================================
// 文件: app/src/main/res/values/styles.xml
// =======================================================
<resources>
<style name="Theme.CountdownSamples" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
</style>
</resources>
// =======================================================
// 文件: app/src/main/res/layout/activity_main.xml
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 通用显示倒计时文本 -->
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="剩余00:00"
android:textSize="24sp"
android:layout_marginBottom="16dp"/>
<!-- 通用开始按钮 -->
<Button
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始倒计时"/>
<!-- 下面依次放各方案的占位容器,仅示例,实际只使用一种方案 -->
<TextView android:layout_marginTop="24dp"
android:text="方案1:Handler + Runnable"/>
<View android:id="@+id/handlerSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案2:CountDownTimer"/>
<View android:id="@+id/countDownTimerSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案3:Timer + TimerTask + Handler"/>
<View android:id="@+id/timerSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案4:ScheduledThreadPoolExecutor"/>
<View android:id="@+id/scheduledSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案5:RxJava.interval"/>
<View android:id="@+id/rxjavaSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案6:Coroutine + Flow"/>
<View android:id="@+id/flowSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案7:Chronometer 控件"/>
<View android:id="@+id/chronoSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案8:AlarmManager"/>
<View android:id="@+id/alarmSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案9:HandlerThread"/>
<View android:id="@+id/handlerThreadSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案10:WorkManager"/>
<View android:id="@+id/workmanagerSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:layout_marginTop="24dp"
android:text="方案11:Compose LaunchedEffect"/>
<View android:id="@+id/composeSection"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>
// =======================================================
// 文件: app/src/main/java/com/example/countdownsamples/MainActivity.kt
// =======================================================
package com.example.countdownsamples
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.*
import android.widget.Button
import android.widget.Chronometer
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.work.*
import com.example.countdownsamples.databinding.ActivityMainBinding
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// ----- 方案1: Handler + Runnable -----
private val handler = Handler(Looper.getMainLooper())
private var handlerRemaining = 60 // 倒计时秒数
private val handlerRunnable = object : Runnable {
override fun run() {
if (handlerRemaining >= 0) {
binding.tvCount.text = "剩余${handlerRemaining}s"
handlerRemaining--
handler.postDelayed(this, 1000)
}
}
}
// ----- 方案2: CountDownTimer -----
private var countDownTimer: CountDownTimer? = null
// ----- 方案3: Timer + TimerTask + Handler -----
private val timerHandler = Handler(Looper.getMainLooper())
private val timer = Timer()
private var timerRemaining = 60
// ----- 方案4: ScheduledThreadPoolExecutor -----
private val scheduledPool = Executors.newScheduledThreadPool(1)
private var scheduledFuture: ScheduledFuture<*>? = null
private var scheduledRemaining = 60
// ----- 方案5: RxJava.interval -----
private val disposables = CompositeDisposable()
// ----- 方案6: Coroutine + Flow -----
private var flowJob: Job? = null
// ----- 方案7: Chronometer 控件 -----
private var chronoBase: Long = 0L
// ----- 方案8: AlarmManager -----
private lateinit var alarmManager: AlarmManager
private lateinit var alarmIntent: PendingIntent
private var alarmRemaining = 60
// ----- 方案9: HandlerThread -----
private lateinit var handlerThread: HandlerThread
private lateinit var htHandler: Handler
private var htRemaining = 60
// ----- 方案10: WorkManager -----
private var workRequestId: UUID? = null
// ----- 方案11: Compose -----
// Compose 在 setContent 中实现
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 统一开始按钮
binding.btnStart.setOnClickListener {
startAll()
}
// 准备 AlarmManager
alarmManager = getSystemService(Context.ALARM_SERVICE)!!
val intent = Intent(this, AlarmReceiver::class.java)
alarmIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
// 注册接收倒计时广播
registerReceiver(alarmReceiver, IntentFilter("COUNTDOWN_ALARM"))
// 准备 HandlerThread
handlerThread = HandlerThread("CountdownThread").apply { start() }
htHandler = Handler(handlerThread.looper)
}
private fun startAll() {
startHandlerCountdown()
startCountDownTimer()
startTimerTask()
startScheduledExecutor()
startRxJava()
startFlow()
startChronometer()
startAlarmCountdown()
startHandlerThreadCountdown()
startWorkManager()
startComposeCountdown()
}
override fun onDestroy() {
super.onDestroy()
// 清理各类资源
handler.removeCallbacks(handlerRunnable)
countDownTimer?.cancel()
timer.cancel()
scheduledFuture?.cancel(true)
disposables.clear()
flowJob?.cancel()
binding.chronometer.stop()
alarmManager.cancel(alarmIntent)
unregisterReceiver(alarmReceiver)
handlerThread.quitSafely()
workRequestId?.let { WorkManager.getInstance(this).cancelWorkById(it) }
}
// === 方案1: Handler + Runnable ===
private fun startHandlerCountdown() {
handlerRemaining = 60
handler.removeCallbacks(handlerRunnable)
handler.post(handlerRunnable)
}
// === 方案2: CountDownTimer ===
private fun startCountDownTimer() {
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(60_000, 1_000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvCount.text = "剩余${millisUntilFinished/1000}s"
}
override fun onFinish() {
binding.tvCount.text = "倒计时结束"
}
}.start()
}
// === 方案3: Timer + TimerTask + Handler ===
private fun startTimerTask() {
timerRemaining = 60
timer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
timerHandler.post {
if (timerRemaining >= 0) {
binding.tvCount.text = "剩余${timerRemaining--}s"
}
}
}
}, 0, 1000)
}
// === 方案4: ScheduledThreadPoolExecutor ===
private fun startScheduledExecutor() {
scheduledRemaining = 60
scheduledFuture?.cancel(true)
scheduledFuture = scheduledPool.scheduleAtFixedRate({
runOnUiThread {
if (scheduledRemaining >= 0) {
binding.tvCount.text = "剩余${scheduledRemaining--}s"
}
}
}, 0, 1, TimeUnit.SECONDS)
}
// === 方案5: RxJava.interval ===
private fun startRxJava() {
disposables.clear()
disposables.add(
io.reactivex.rxjava3.core.Observable.interval(0,1, TimeUnit.SECONDS)
.take(61)
.map { 60 - it }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { sec ->
binding.tvCount.text = "剩余${sec}s"
}
)
}
// === 方案6: Coroutine + Flow ===
private fun startFlow() {
flowJob?.cancel()
flowJob = lifecycleScope.launch {
(0..60).asFlow()
.map { 60 - it }
.onEach { delay(1000) }
.collect { sec ->
binding.tvCount.text = "剩余${sec}s"
}
}
}
// === 方案7: Chronometer 控件 ===
private fun startChronometer() {
chronoBase = SystemClock.elapsedRealtime() + 60_000
binding.chronometer.base = chronoBase
binding.chronometer.format = "剩余%s"
binding.chronometer.setOnChronometerTickListener {
val sec = (chronoBase - SystemClock.elapsedRealtime()) / 1000
if (sec < 0) binding.chronometer.stop()
}
binding.chronometer.start()
}
// === 方案8: AlarmManager ===
private fun startAlarmCountdown() {
alarmRemaining = 60
alarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 1000,
alarmIntent
)
}
private val alarmReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (alarmRemaining >= 0) {
binding.tvCount.text = "剩余${alarmRemaining--}s"
alarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 1000,
alarmIntent
)
}
}
}
// === 方案9: HandlerThread ===
private fun startHandlerThreadCountdown() {
htRemaining = 60
htHandler.post(object: Runnable {
override fun run() {
if (htRemaining >= 0) {
runOnUiThread { binding.tvCount.text = "剩余${htRemaining}s" }
htRemaining--
htHandler.postDelayed(this, 1000)
}
}
})
}
// === 方案10: WorkManager ===
private fun startWorkManager() {
val data = Data.Builder().putInt("remaining", 60).build()
val req = OneTimeWorkRequestBuilder<CountdownWorker>()
.setInputData(data)
.setInitialDelay(1, TimeUnit.SECONDS)
.build()
workRequestId = req.id
WorkManager.getInstance(this).enqueue(req)
WorkManager.getInstance(this).getWorkInfoByIdLiveData(req.id)
.observe(this) { info ->
val rem = info.progress.getInt("remaining", -1)
if (rem >= 0) binding.tvCount.text = "剩余${rem}s"
if (info.state.isFinished) binding.tvCount.text = "倒计时结束"
}
}
// === 方案11: Compose LaunchedEffect ===
private fun startComposeCountdown() {
setContent {
CountdownComposeSample()
}
}
}
// =======================================================
// 文件: app/src/main/java/com/example/countdownsamples/CountdownWorker.kt
// =======================================================
package com.example.countdownsamples
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay
class CountdownWorker(ctx: Context, params: WorkerParameters): CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
var rem = inputData.getInt("remaining", 0)
while (rem >= 0) {
setProgress(Data.Builder().putInt("remaining", rem--).build())
delay(1000)
}
return Result.success()
}
}
// =======================================================
// 文件: app/src/main/java/com/example/countdownsamples/AlarmReceiver.kt
// =======================================================
package com.example.countdownsamples
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class AlarmReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// nothing, handled in MainActivity
}
}
// =======================================================
// Kotlin Compose 部分 —— 文件: CountdownComposeSample.kt
// =======================================================
package com.example.countdownsamples
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
fun CountdownComposeSample() {
var seconds by remember { mutableStateOf(60) }
LaunchedEffect(Unit) {
while (seconds >= 0) {
delay(1000)
seconds--
}
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "剩余${seconds}s")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { seconds = 60 }) { Text("重置") }
}
}
六、代码解读
方案1:Handler + Runnable
-
核心:使用
Handler.postDelayed()
每秒循环执行Runnable
,在 UI 线程更新文本。 -
优点:原生、轻量、简单;
-
缺点:手动清理回调,易漏调用导致内存泄漏。
方案2:CountDownTimer
-
核心:系统提供的
CountDownTimer(millisInFuture, countDownInterval)
,内部封装了Handler
。 -
优点:自动停止,回调清晰;
-
缺点:粒度受限于
interval
,间隔小于 1s 时不一→稳定。
方案3:Timer + TimerTask + Handler
-
核心:Java
Timer.scheduleAtFixedRate()
产生后台线程,并通过Handler
切换到 UI 线程。 -
优点:后台执行;
-
缺点:
Timer
在定时任务抛异常时会终止,且手动清理。
方案4:ScheduledThreadPoolExecutor
-
核心:
ScheduledExecutorService.scheduleAtFixedRate()
更健壮,支持线程池。 -
优点:异常隔离、更可靠;
-
缺点:线程资源管理需手动。
方案5:RxJava.interval
-
核心:
Observable.interval(0,1,TimeUnit.SECONDS)
生成流,take(61)
限制次数,map
计算剩余秒。 -
优点:可取消订阅、易组合;
-
缺点:引入 Rx 依赖。
方案6:Coroutine + Flow
-
核心:
(0..60).asFlow().map { 60-it }.onEach { delay(1000) }
,collect
更新 UI。 -
优点:Kotlin 原生、简洁、无需额外库;
-
缺点:需配合 LifecycleScope。
方案7:Chronometer
-
核心:系统计时控件,设置
base = SystemClock.elapsedRealtime() + futureMillis
,format
自定义显示。 -
优点:自动更新;
-
缺点:仅支持正向计时,通过计算剩余实现变通。
方案8:AlarmManager
-
核心:通过闹钟广播每秒触发,适合在应用后台仍需准确倒计时场景。
-
优点:系统托管、重启后依旧有效;
-
缺点:高频率(1s)使用不当会较耗电,不推荐在前台逻辑。
方案9:HandlerThread
-
核心:自建
HandlerThread
,通过其Looper
调度延时消息。 -
优点:隔离主线程,可靠;
-
缺点:需手动管理线程生命周期。
方案10:WorkManager
-
核心:后台工作管理框架,借助
CoroutineWorker
实现秒级进度推送。 -
优点:Lifecycle 感知,失败重试;
-
缺点:精度受系统策略影响,秒级不够精确。
方案11:Jetpack Compose
-
核心:
LaunchedEffect
内启动协程与delay
,更新state
驱动 UI 重组。 -
优点:Compose 原生、声明式;
-
缺点:仅限 Compose 项目。
七、性能与优化
-
清理回调
-
在
onDestroy()
中移除所有回调与取消订阅,避免内存泄漏。
-
-
精度 vs 电量
-
高精度(1s 级)倒计时应优先使用
Handler
、ScheduledExecutor
、Flow
; -
后台长时(分钟/小时)倒计时建议
AlarmManager
或WorkManager
。
-
-
线程隔离
-
尽量避免在主线程执行耗时操作(虽倒计时本身轻量,但与其他逻辑耦合可能阻塞 UI)。
-
-
UI 粒度
-
手机屏幕刷新率约 60Hz,1s 更新一次足矣;高频更新没意义。
-
-
容错
-
Timer
抛异常时会中断,推荐替换为ScheduledExecutorService
。
-
-
节省依赖
-
若已使用 RxJava 或 Coroutines,可优先使用内置方案,无需额外引入。
-
八、项目总结与拓展
1. 总结
-
本文覆盖了11 种 Android 倒计时实现方案,从最基础的
Handler
到现代的Coroutine+Flow
与 Compose,涵盖了 UI 控件、第三方库、系统服务、后台框架等。 -
各方案源码全整合,详尽注释,便于读者复制粘贴并根据项目需求选型。
-
介绍了性能考量、生命周期管理和优化策略,帮助你在实际项目中稳健落地。
2. 拓展方向
-
支持暂停/恢复:维护剩余时间的状态,将倒计时逻辑抽象为可暂停/恢复的组件;
-
多任务倒计时管理:管理多个同时运行的倒计时任务,使用
Map<id, Job>
或CompositeDisposable
; -
进度条倒计时:将文本倒计时与
ProgressBar
、Lottie
动画结合,提升交互体验; -
跨进程/跨应用倒计时:例如闹钟或通知中显示剩余时间;
-
可视化测试:对比不同方案在高负载或低电量下的表现,做压力测试。
九、FAQ
Q1:倒计时精准度受哪些因素影响?
A1:主线程消息延迟、Doze 模式(后台省电)、GC 暂停、Timer 异常等,短时建议 Handler 或 Coroutine,长时建议 AlarmManager。
Q2:为什么 CountDownTimer 可能不准确?
A2:onTick()
回调实际执行时间依赖 Handler 调度,间隔不保证严格 1000ms。
Q3:如何在 Activity 切换/旋转时保留倒计时状态?
A3:将剩余时间保存到 ViewModel
或 onSaveInstanceState
,重建 Activity 后恢复。
Q4:WorkManager 是否适合秒级倒计时?
A4:不建议。WorkManager 设计用于可靠后台任务,系统会批量调度,精度不保证。
Q5:Compose 如何暂停倒计时?
A5:将 LaunchedEffect
key 设为 seconds
,变更时重启协程,可通过状态变量控制暂停。