Flow 基础:07. Flow 构建器 (Flow Builders)

本篇解析 example-flow-07.kt。除了 flow { ... },Kotlin 还提供了多种便捷的构建器来创建流。

1. 常见构建器

  • **asFlow()**:扩展函数,可将 RangeIterable(如 List、Set)或 Sequence 转换为 Flow。
  • **flowOf(…)**:定义一个发射固定值集的流。

2. 代码解析

1
2
3
4
fun main() = runBlocking {
// 将整数范围转换为流并收集
(1..3).asFlow().collect { value -> println(value) }
}

3. 开发者感悟

在实际开发中,如果你已经有一组现成的数据(例如从数据库加载的 List),或者只是想简单地测试某个流操作符,asFlow()flowOf() 是最快捷的选择。它们省去了手动编写 emit 的麻烦。

协程进阶:07. 超时自动取消 (withTimeout)

本篇解析 example-cancel-07.kt,介绍如何给任务设定执行期限。

1. 核心构建器:withTimeout

withTimeout 用于在指定时间内执行一段逻辑。

  • 特性:如果代码块在规定时间内没有运行完,它会立即取消该协程并抛出 TimeoutCancellationException

2. 代码解析

1
2
3
4
5
6
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
  • 由于每次 delay 500ms,在打印两次(1000ms)后,第三次打印前就会触发 1300ms 的阈值。
  • 程序会因为抛出异常而终止。

3. 开发者感悟

withTimeout 是处理网络请求或数据库查询时的必备工具。但要注意,因为它会抛出异常,通常需要配合 try-catch 使用,否则可能会导致应用崩溃。

协程进阶:通道 08. 带缓冲的通道 (Buffered Channels)

本篇解析 example-channel-08.kt。探讨如何平衡生产与消费的速度。

1. 核心概念:缓冲区 (Buffer)

默认通道是“会合”的(无缓冲区)。带缓冲的通道允许发送者在缓冲区未满之前,持续发送数据而不需要挂起等待接收者。

规则解析:

  • 未满时send() 立即执行并返回,不会挂起。
  • 满时send() 会挂起,直到缓冲区有空余位置(被接收者取走)。

2. 代码解析

1
2
3
4
5
6
7
val channel = Channel<Int>(4) // 创建容量为 4 的缓冲区
val sender = launch {
repeat(10) {
println("Sending $it")
channel.send(it) // 发射第 5 个时由于缓冲满了,会在这里挂起
}
}

3. 开发者感悟

缓冲通道是并发系统中的“减震器”。它允许生产和消费两端在一定程度上解耦。在 Android 开发中,如果你有大量高频传感器数据产出,但 UI 渲染较慢,设置一个适当容量的缓冲区能显著提高系统的整体响应性.

协程进阶:上下文 08. 协程命名 (CoroutineName)

本篇解析 example-context-08.kt。探讨如何更好地给异步任务“贴标签”。

1. 核心概念:CoroutineName

CoroutineName 用于给协程起一个名字,方便在调试时区分。

代码解析

1
2
3
launch(CoroutineName("v1coroutine")) {
log("Computing v1") // 打印出的日志会包含 [main @v1coroutine]
}

2. 开发者感悟

在处理复杂的 UI 更新或数据同步时,通常会同时跑好几个协程。通过 CoroutineName,你可以一眼在 Logcat 中认出是谁在报错或谁在打印。这在大型 Android 项目的维护中非常有价值。

Flow 基础:08. Map 与 Filter 操作符

本篇解析 example-flow-08.kt。探讨 Flow 中最基础的转换操作符.

1. 核心概念

Flow 的操作符命名与 ListSequence 非常相似,例如 map(转换)和 filter(过滤)。

关键区别:

  • 支持挂起:Flow 操作符内部的代码块可以调用挂起函数

2. 代码解析

1
2
3
4
5
6
7
8
9
10
suspend fun performRequest(request: Int): String {
delay(1000) // 模拟一个 1 秒钟的网络请求
return "response $request"
}

fun main() = runBlocking {
(1..3).asFlow()
.map { request -> performRequest(request) } // 这里的 map 内部调用了挂起函数
.collect { response -> println(response) }
}
  • 串行执行:默认情况下,每个 map 转换都会等待前一个转换完成。在这个例子中,每隔 1 秒产出一个结果,总共耗时约 3 秒。

3. 开发者感悟

Flow 最大的魅力就在于此:它让你能用写同步集合代码的方式(一个 map 搞定),去处理原本非常复杂的异步回调逻辑。

协程进阶:08. 超时安全返回 (withTimeoutOrNull)

本篇解析 example-cancel-08.kt,介绍一种更温和的超时处理方式.

1. 核心构建器:withTimeoutOrNull

相比于 withTimeout 会抛出异常,withTimeoutOrNull 在超时发生时会返回 null

代码解析

1
2
3
4
5
6
7
8
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 正常结束时返回这个字符串
}
println("Result is $result")
  • 由于 1300ms 后任务被自动取消,块内的 “Done” 无法被返回。
  • result 最终被赋值为 null

2. 为什么使用它?

在很多业务场景下,超时是一个预期内的情况,而不是一个“程序错误”。

  • 例如:尝试请求网络数据,如果 3 秒没返回就显示缓存。
  • 使用 withTimeoutOrNull 可以省去繁琐的 try-catch 模板代码,让逻辑更简洁。

3. 开发者感悟

withTimeoutOrNull 是协程库提供的一个非常人性化的设计。它体现了 Kotlin 追求“代码简洁”和“空安全”的理念。在编写 API 调用逻辑时,它是我的首选。

协程进阶:通道 09. 通道的公平性 (Fairness)

本篇解析 example-channel-09.kt。探讨多个协程竞争通道时的分配原则。

1. 核心概念:公平性原则

当多个协程在同一个通道上进行发送和接收时,Channel 遵循先到先得 (FIFO) 的原则。

特点:

  • 非霸占性:不会出现一个协程一直霸占通道,而其他协程一直“挨饿”的情况。
  • 轮询效果:如果有多个消费者在等待,它们会按照请求接收的先后顺序,轮流获得发射出的数据。

2. 案例分析 (Ping-Pong)

两个玩家(协程)共用一个乒乓球台(Channel)。

  • 执行逻辑
    1. A 发球 -> B 接收。
    2. B 回球 -> A 接收。
    3. 循环往复。
  • 结果:输出呈现完美的 ping, pong, ping, pong 节奏,充分体现了 Channel 对多个并发协程的公平调度。

3. 开发者感悟

Channel 的公平性是构建高可靠并发系统的基础。在 Android 中,如果你有多个后台同步任务需要访问同一个数据源,利用 Channel 这种“排队领取”的特性,可以天然地避免数据错乱和线程竞争,让业务逻辑变得简单且可预测。

协程进阶:上下文 09. 合并上下文元素

本篇解析 example-context-09.kt。学习如何灵活定义协程环境。

1. 核心操作符:+

你可以使用 + 操作符来组合多个上下文元素。

代码解析:

1
2
3
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
  • 组合结果:该协程会运行在 Dispatchers.Default 线程池中,且其调试名称为 “test”。

2. 开发者感悟

+ 号操作符让上下文的管理变得极其灵活。你可以根据需要,将调度器、Job、名称以及异常处理器拼接在一起。这就像是拼积木,你可以随心所欲地定制每一个异步任务的执行环境。

Flow 基础:09. Transform 通用转换操作符

本篇解析 example-flow-09.kt。学习 Flow 中最灵活的转换工具:transform

1. 核心概念

transform 操作符是 mapfilter 的超集。它允许你:

  1. 多次发射:在处理一个输入值时,可以调用多次 emit
  2. 条件发射:模拟 filter 的逻辑。
  3. 异步发射:内部可随意调用挂起函数。

2. 代码解析

1
2
3
4
5
6
(1..3).asFlow()
.transform { request ->
emit("Making request $request") // 第一次发射:日志信息
emit(performRequest(request)) // 第二次发射:真实响应
}
.collect { println(it) }
  • 灵活性:在这个例子中,每个请求都对应了两条产出。这种“一变多”的逻辑是 map 无法实现的(map 只能一变一)。

3. 开发者感悟

在处理复杂的 API 转换时,transform 非常好用。比如在请求之前先发射一个“加载中”状态,请求完再发射结果。它比 map 更强大,能让你更精细地控制数据流的输出。

协程进阶:09. 并发下的资源安全陷阱

本篇解析 example-cancel-09.kt,探讨超时与资源管理之间的一个隐蔽 Bug。

1. 核心场景

在一个循环中启动大量协程,每个协程都会:

  1. withTimeout(60):设置 60ms 超时。
  2. delay(50):模拟耗时操作。
  3. Resource():分配资源。
  4. resource.close():释放资源。

2. 核心问题:资源泄漏

你会发现程序运行完后,acquired 变量的值并不为 0。

  • 原因:由于超时是异步且并发发生的。可能在 Resource() 构造函数刚刚执行完,资源已经分配(acquired++),但还没来得及赋值给变量或执行下一步时,超时信号传来了。
  • 后果:协程被取消并抛出异常,resource.close() 永远没机会被执行。

3. 开发者感悟

这是一个非常经典的并发竞争问题。它提醒我们:分配资源的操作释放资源的操作之间,如果存在被异步取消的可能性,就必须采取更严密的保护措施。