1. 概述
在开发过程中,我们经常需要执行周期性任务,比如定时更新数据、传感器监控、发送通知等。这类需求在各类应用中都非常常见。
本文将介绍几种在 Kotlin 中实现重复任务调度的方式,包括使用 Java 提供的 Timer
和 ScheduledExecutorService
,以及 Kotlin 自带的协程(Coroutines)和 Flow。
我们将会看到每种方式的使用方式、适用场景以及需要注意的地方。对于有经验的开发者来说,这是一份实用的参考指南。
2. 使用 Timer.schedule()
Timer
是 Java 中 java.util
包下的一个类,可以用于定时执行任务,也可以周期性执行任务:
val timer = Timer()
每个 Timer
实例都在一个后台线程中顺序执行任务。如果某个任务执行时间过长,会影响后续任务的执行。因此,任务本身应尽量轻量。
我们可以通过 schedule()
方法创建一个定时任务:
timer.schedule(object : TimerTask() {
override fun run() {
println("Timer ticked!")
}
}, 0, 1000)
Kotlin 提供了更简洁的封装方式,可以直接使用 Lambda:
timer.schedule(0L, 1000L) {
println("Timer ticked!")
}
最后,如果需要停止任务,调用 cancel()
方法即可:
timer.cancel()
✅ 优点:简单易用
❌ 缺点:单线程执行,任务异常会中断后续任务
3. 使用 ScheduledExecutorService
ScheduledExecutorService
是 java.util.concurrent
包中的接口,允许我们:
- 定时执行任务
- 周期性执行任务
它通过线程池来执行任务,因此比 Timer
更高效:
val scheduler = Executors.newScheduledThreadPool(1)
我们使用 scheduleAtFixedRate()
方法来执行周期性任务:
scheduler.scheduleAtFixedRate({
println("Complex task completed!")
}, 0, 1, TimeUnit.SECONDS)
当不再需要执行任务时,记得调用 shutdown()
关闭线程池:
scheduler.shutdown()
✅ 优点:支持多线程、更灵活的调度机制
❌ 缺点:需要手动管理线程池生命周期
4. 使用协程(Coroutines)
Kotlin 协程是一种轻量级的并发模型,非常适合处理异步任务,也适用于周期性任务的调度。
4.1. 使用 repeat()
和 delay()
Kotlin 提供了两个非常实用的函数:
repeat(n)
:重复执行某段代码 n 次delay(time)
:延迟执行当前协程,不阻塞线程
示例代码如下:
var count = 0
repeat(10) {
count++
println("Timer ticked! $count")
delay(1000.milliseconds)
}
assertEquals(10, count)
这段代码会执行 10 次打印,并在每次之间延迟 1 秒。
4.2. 使用 withTimeout()
withTimeout()
可用于限制协程执行的最大时间,超时后会抛出 TimeoutCancellationException
:
var count = 0
assertThrows<TimeoutCancellationException> {
withTimeout(5000.milliseconds) {
while (true) {
count++
println("Waiting for timeout")
delay(1000.milliseconds)
}
}
}
assertEquals(5, count)
⚠️ 注意:使用 withTimeout()
时,一定要配合 try-catch
或者在测试中使用 assertThrows
。
✅ 优点:非阻塞、轻量级、支持异常处理
❌ 缺点:需要理解协程上下文和生命周期管理
5. 使用协程 Flow
Kotlin Flow 是基于协程构建的响应式流处理库,非常适合处理周期性、流式数据任务。
Flow 是 冷流(Cold Flow),只有在调用 collect()
时才会开始执行。
我们可以构建一个每秒发射一次数据的 Flow:
val flow = flow {
while (true) {
emit(Unit)
delay(1000.milliseconds)
}
}
5.1. 使用 collect()
collect()
是 Flow 中用于消费数据流的方法:
var count = 0
flow.collect {
count++
println(count)
}
这段代码会无限打印递增的数字,每秒一次。
5.2. 使用 take()
和 collect()
如果我们只想执行一定次数,可以使用 take()
:
flow.take(10).collect {
count++
println("Task executed $count")
}
这段代码只会执行 10 次任务,之后自动结束。
✅ 优点:适合处理周期性数据流、可组合性强
❌ 缺点:需要理解 Flow 的生命周期和取消机制
6. 总结
我们介绍了几种在 Kotlin 中实现周期性任务调度的方式:
方法 | 适用场景 | 特点 |
---|---|---|
Timer |
简单任务 | 单线程,容易踩坑 |
ScheduledExecutorService |
多线程任务 | 灵活但需手动管理 |
Coroutines |
协程任务 | 非阻塞、轻量级 |
Flow |
流式任务 | 适合数据流处理 |
对于简单任务,Timer
足够使用;如果任务复杂度上升,建议使用 ScheduledExecutorService
或协程;如果涉及流式数据或周期性数据处理,推荐使用 Flow
。
所有示例代码都可以在 GitHub 仓库 中找到。
✅ 建议:优先使用协程和 Flow,避免使用 Timer
,因为其存在单线程、异常中断等问题,容易成为潜在的坑点。