Flow 基础:01. 集合与遍历

本篇解析 example-flow-01.kt。在深入 Flow 之前,我们先回顾一下最基础的集合处理。

1. 代码解析

1
2
3
4
5
fun simple(): List<Int> = listOf(1, 2, 3)

fun main() {
simple().forEach { value -> println(value) }
}

2. 特点

  • 同步性listOf 会立即创建并填充所有元素。
  • 一次性返回:函数 simple() 必须等待所有元素准备就绪后,才能一次性返回整个列表。

3. 局限性

如果计算每个元素都需要耗时操作(如网络请求),那么调用者必须在拿到结果前等待所有耗时操作完成。这在处理异步流式数据时效率较低。

协程进阶:01. 基本取消操作

本篇解析 example-cancel-01.kt,介绍如何手动停止一个正在运行的协程。

1. 核心操作

  • **job.cancel()**:请求取消协程。它会发送一个取消信号,但协程不会立即“死去”。
  • **job.join()**:挂起当前线程/协程,直到目标协程彻底结束。
  • **job.cancelAndJoin()**:上述两个操作的组合快捷函数。

2. 为什么能取消?

在这个例子中,协程内部调用了 delay()

  • delay 是一个可取消的挂起函数
  • 当它检测到取消信号时,会抛出 CancellationException,从而中断协程。

3. 执行流程

  1. 启动协程打印 “sleeping…”。
  2. 主线程延迟 1.3 秒。
  3. 调用 cancel()
  4. 协程在下一个 delay 处检测到信号,停止执行。
  5. 打印 “Now I can quit.”。

Kotlin 协程基础 01:启动第一个协程

本篇文档解析 example-basic-01.kt,介绍协程的最基本启动方式和核心概念。

1. 核心组件解析

runBlocking

  • 作用:连接“普通世界”与“协程世界”的桥梁。
  • 特性:它会阻塞当前执行它的线程,直到其内部所有的协程任务执行完毕。
  • 使用场景:通常用于 main 函数、单元测试,或将阻塞式 API 与协程代码衔接。

launch

  • 作用:启动一个新的协程,但不等待其结果。
  • 返回值:返回一个 Job 对象(在后续章节详细讨论)。
  • 特性:它是“发射后不管”的模式。

delay

  • 作用:非阻塞式的挂起函数。
  • 特性:它会让当前协程暂停执行一段时间,但不会阻塞底层线程。在等待期间,线程可以去执行其他协程。

2. 代码执行流程

1
2
3
4
5
6
7
fun main() = runBlocking {
launch {
delay(1000L) // 协程挂起,释放线程
println("World!")
}
println("Hello") // 线程立即执行这一行
}
  1. runBlocking 启动,阻塞主线程。
  2. launch 启动一个子协程,准备延迟 1 秒。
  3. 主流程不等待 launch,直接打印 Hello
  4. 1 秒后,子协程恢复执行,打印 **World!**。
  5. 所有任务完成,runBlocking 结束,程序退出。

3. 开发者感悟

runBlocking 就像是一个特殊的容器,它告诉线程:“在我里面的事情没干完之前,你不准走。”而 launch 则是容器里的一个小工,它在后台干活,不影响容器里的主进度。

1. 两数之和

题目

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

1
2
输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

1
2
输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 10^4
  • -109 <= nums[i] <= 10^9
  • -109 <= target <= 10^9
  • 只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

简介

1
2
3
4
这道题的关键在于对 hashmap 查找时间复杂度 O(1) 的应用
关键词:
HashMap<Key, Value>
hashmap.containsKey(Value)

正文

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
fun twoSum(nums: IntArray, target: Int): IntArray {
val hashmap = HashMap<Int, Int>()
for(i in nums.indices){
val complement = target - nums[i]
if(hashmap.containsKey(complement)){
return intArrayOf(hashmap[complement]!!, i)
}
hashmap[nums[i]] = i
}
return intArrayOf()
}
}

协程进阶:监督 02. supervisorScope 作用域

本篇解析 example-supervision-02.kt。学习如何在局部代码块中应用监督机制。

1. 核心概念:supervisorScope

supervisorScope 是一个作用域构建器,它会创建一个使用 SupervisorJob 的作用域。

特点:

  • 局部隔离:它只作用于该块内部的子协程。
  • 子任务独立性:如果块内的一个子任务失败了,它不会导致该作用域内的其他子任务被取消。
  • 父级取消依然有效:如果 supervisorScope 所在的外部协程被取消,或者 supervisorScope 内部的代码抛出了异常,所有子协程依然会被取消。

2. 代码解析

1
2
3
4
5
6
7
8
9
10
11
12
try {
supervisorScope {
val child = launch {
// ... 正在运行的子协程
}
yield()
println("Throwing an exception from the scope")
throw AssertionError() // 作用域自身抛出异常
}
} catch(e: AssertionError) {
println("Caught an assertion error") // 子协程会被取消,因为作用域本身报错了
}

3. 开发者感悟

supervisorScopecoroutineScope 的“监督版”。在 Android 开发中,当你需要在一个挂起函数内部并行处理多项不相关的任务(如同时下载 3 张图片),且希望某一张下载失败不要影响其他的下载时,请务必使用 supervisorScope

协程进阶:同步 02. Volatile 的局限性

本篇解析 example-sync-02.kt。探讨为什么 @Volatile 无法解决并发累加问题。

1. 核心概念:Volatile 的作用

精 Java/Kotlin 中,@Volatile 关键字保证了变量的可见性(即一个线程修改了值,其他线程能立即看到最新值),但它不保证原子性

2. 为什么失败?

对于 counter++ 操作:

  1. 读取当前值。
  2. 计算新值(+1)。
  3. 写回新值。

虽然每个线程都能看到最新的 counter,但当两个线程同时读取了值(例如都是 10),它们都会计算出 11 并写回,导致一次加法丢失。

3. 开发者感悟

@Volatile 仅适用于“一个线程写,多个线程读”的标志位场景。在涉及多线程同时修改(读-写)同一个变量的场景下,它完全无能为力。不要被它的名字迷惑。

协程进阶:选择 02. 处理通道关闭 (onReceiveCatching)

本篇解析 example-select-02.kt。探讨在选择过程中如何优雅地处理通道关闭。

1. 核心操作:onReceiveCatching

当通道可能被关闭时,普通的 onReceive 在接收到关闭信号时会抛出异常。onReceiveCatching 则会返回一个 ChannelResult

优势:

  • 异常安全:它不会抛出异常。
  • 状态判断:你可以通过 isSuccessgetOrNull() 来判断是拿到了真实数据,还是通道已经关闭。

2. 代码解析

1
2
3
4
5
6
7
select<String> {
a.onReceiveCatching { it ->
val value = it.getOrNull()
if (value != null) "a -> '$value'" else "Channel 'a' is closed"
}
// ...
}

3. 开发者感悟

在处理诸如“多源下载”的任务时,有些下载流可能先结束并关闭。使用 onReceiveCatching 能够让你在不崩溃的情况下,从容地处理掉这些已经关闭的资源,并继续监听其他仍在运行的流。

协程进阶:通道 02. 关闭与遍历

本篇解析 example-channel-02.kt。学习如何优雅地结束通道通信。

1. 核心操作:close()

当生产者不再发送数据时,应调用 close()

  • 信号传递close() 会向通道发送一个特殊的“结束令牌”。
  • 接收端感知:接收端在处理完缓冲区剩余数据后,会自动感知到通道已关闭并停止等待。

2. 核心语法:for 循环遍历

你可以像遍历 List 一样直接使用 for (y in channel)

  • 自动结束:当通道被 close() 且数据取完后,循环会自动终止。

3. 开发者感悟

close() 是一种契约。它告诉消费者:“货发完了,你可以下班了。”在 Android 中,如果你使用 Channel 传递文件上传进度,上传完成后务必调用 close(),这样 UI 层的监听循环才能正常退出,避免不必要的挂起。

协程进阶:组合 02. async 异步并发

本篇解析 example-compose-02.kt。学习如何让不相干的任务并行运行.

1. 核心操作符:async

asynclaunch 类似,都会启动一个新的协程。

关键区别:

  • 返回值async 返回一个 Deferred<T>,它代表一个未来的结果。
  • 获取结果:通过 await() 挂起并等待结果。
  • 并发性:在 await() 之前启动多个 async,它们会并发执行

2. 代码解析

1
2
3
4
5
6
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
// 结果:Completed in 1000+ ms
  • 对比顺序执行:时间从 2000ms 减少到了 1000ms。

3. 开发者感悟

async 是协程中处理并发的核心手段。在 Android 中,如果你需要同时请求用户信息和通知列表,请务必使用 async 并行处理。只有当你真正需要结果时,才调用 await()

协程进阶:上下文 02. 非受限调度器 (Dispatchers.Unconfined)

本篇解析 example-context-02.kt。探讨一种特殊的调度器行为。

1. 核心概念:Dispatchers.Unconfined

Dispatchers.Unconfined 是一个非受限的调度器。

特点:

  • 立即执行:协程在调用者的线程中立即开始执行。
  • 恢复位置不确定:当协程遇到第一个挂起点并恢复后,它会在恢复它的挂起点的线程上继续执行。
  • 不建议用于普通业务:它不适合一般的异步任务,因为它可能会导致某些逻辑在不可预知的线程(如执行 delay 的共享线程池)中运行。

2. 代码解析

1
2
3
4
5
launch(Dispatchers.Unconfined) { 
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
  • 执行逻辑
    1. 开始时在 main 线程运行。
    2. delay 结束后,可能在 kotlinx.coroutines.DefaultExecutor 线程中恢复。

3. 开发者感悟

Unconfined 主要用于某些对性能极其敏感且不需要切换线程开销的边缘场景。在 Android 实际开发中,应优先选择 DefaultIOMain,以保证线程的可预测性。