0%

kotlin协程讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
目录

协程与调度器总览(Main / IO / Default / Unconfined)—— 为什么需要“切线程”

必备第三方库(Gradle 依赖)及作用说明

在协程中切换线程:withContext、launch 的调度器参数、flowOn 等详解 + 代码示例

常见集成示例:Retrofit(suspend)、Room(suspend / Flow)、OkHttp、Paging3、Lifecycle/Ktx 等

ViewModel + Repository + Retrofit + Room(完整代码片段)

异常处理、取消、Supervisor、结构化并发、性能优化与常见坑



1. 协程与调度器总览(为什么需要切线程)

在 Android 上,有两个核心概念要理解:

主线程(Main / UI 线程):负责绘制 UI、处理用户事件。任何长时间运行或阻塞的操作(网络、数据库、文件、复杂计算)放在主线程会导致 ANR(应用无响应)或界面卡顿。

后台线程(IO / Default):用于耗时操作。协程通过调度器(Dispatcher)将协程切换到合适线程池执行,从而避免卡 UI。

常用调度器:

Dispatchers.Main:运行在主线程(UI)。用于更新 UI、与 Android 框架交互。

Dispatchers.IO:用于网络、文件、数据库等 IO 密集型任务。基于一个可扩展线程池(适合等待型、阻塞型 IO)。

Dispatchers.Default:用于 CPU 密集型任务(复杂计算、排序、图像处理等)。

Dispatchers.Unconfined:常用于某些测试或非常特殊的案例——不是常规选择,可能在调度上下文上行为不稳定。

“切线程” 的常见方式:在协程内部使用 withContext(...) { ... } 来切换执行上下文(线程池)。这是协程推荐的显式切换方式,语义清晰、安全。

2. 必备第三方库(Gradle 依赖)及作用说明

下面是一个常见 Android 项目会用到的协程相关依赖(Kotlin DSL 或 Gradle Groovy 均可):

// Kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" // 提供 Dispatchers.Main

// AndroidX lifecycle + ktx
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" // viewModelScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" // lifecycleScope

// Retrofit + Coroutine (suspend support)
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0" // 或 converter-gson
implementation "com.squareup.okhttp3:okhttp:4.11.0"

// Room
implementation "androidx.room:room-runtime:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
implementation "androidx.room:room-ktx:2.5.2" // support suspend & Flow

// Kotlin Flow + StateFlow 是 Kotlin stdlib coroutines 的一部分,无额外依赖

// Paging 3(可选)
implementation "androidx.paging:paging-runtime-ktx:3.2.0"


说明:

kotlinx-coroutines-android 提供 Dispatchers.Main 的实现(基于 Android Looper)。

lifecycle-viewmodel-ktx 提供 viewModelScope(自动随 ViewModel 清理)。

retrofit + suspend:Retrofit 本身会把 suspend 接口函数当成挂起点(配合 OkHttp)。你可以直接在 Retrofit 的 service 定义 suspend fun getUsers(): List<User>。

room-ktx 提供 suspend DAO 方法 + 返回 Flow<T> 的集成,便于结合协程使用。

3. 在协程中切换线程:withContext、launch 的调度器参数、flowOn 等详解
3.1 launch 本身接收 Dispatcher
viewModelScope.launch(Dispatchers.IO) {
// 协程的默认执行上下文是 IO(适合在这里直接做网络/DB)
val data = repository.loadFromNetwork()
withContext(Dispatchers.Main) {
// 回到主线程更新 UI
_uiState.value = data
}
}

3.2 withContext:显式切换并返回结果(推荐)

withContext 会挂起当前协程并把代码块切到目标调度器执行,执行完再切回调用者上下文(如果需要)。

viewModelScope.launch {
// 默认上下文为 Main(viewModelScope)
val data = withContext(Dispatchers.IO) {
api.getUsers() // 在 IO 池运行
}
// 回到 Main 继续执行
textView.text = data[0].name
}


推荐模式:在 ViewModel 中用 viewModelScope.launch { val r = withContext(Dispatchers.IO) { ... } ... }
这样将 IO 逻辑隔离在 withContext(Dispatchers.IO) 块中,方便单元测试与可读性。

3.3 async + await 与并发

当需要并发请求时使用 async(注意 async 默认是懒或立即启动取决于参数):

viewModelScope.launch {
val deferred1 = async(Dispatchers.IO) { api.loadA() }
val deferred2 = async(Dispatchers.IO) { api.loadB() }
val a = deferred1.await()
val b = deferred2.await()
// 合并结果
}


或更安全的 coroutineScope 包裹并发任务实现结构化并发:

viewModelScope.launch {
coroutineScope {
val a = async(Dispatchers.IO) { api.loadA() }
val b = async(Dispatchers.IO) { api.loadB() }
// 如果一个失败,coroutineScope 会取消其它协程
}
}

3.4 Flow 的 flowOn 与 collect 在 Main/IO 的区别

flow {} 定义的数据生产(emit)部分默认在调用者的上下文执行,flowOn() 用来改变上游执行调度器:

val dataFlow = flow {
val data = db.query() // 这是生产端,应该在 IO
emit(data)
}.flowOn(Dispatchers.IO) // 指定上游在 IO 执行

lifecycleScope.launch {
dataFlow.collect { data ->
// collect 在 Main(当前作用域)执行,可直接更新 UI
}
}


注意:flowOn 只影响上游发射;collect 的代码(下游)仍在调用 collect 时的上下文运行。

3.5 Dispatchers.Main.immediate

Main.immediate 会尝试在当前主线程立即执行,如果当前已经在主线程则不会重新调度。适用于需要避免重复切换但要保证在主线程执行的场景(较少用)。

4. 常见集成示例(更具体)
4.1 Retrofit + suspend

定义接口:

interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}


直接在协程中调用 api.getUsers() —— Retrofit 会在 OkHttp 的线程池执行网络请求并恢复协程。

注意:不要在 suspend 中又使用回调(反模式),把回调包装成 suspend 才行。

4.2 Room + suspend / Flow

DAO 示例:

@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun observeAll(): Flow<List<User>> // 推荐:Flow 自动发射 DB 更新

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>) // suspend 插入
}

4.3 Retrofit + OkHttp 拦截器(logging、token 等)

OkHttp 是在网络层处理的,与协程无直接关系,但在协程中使用 suspend 的 Retrofit 时,请确保 OkHttp 的拦截器不会阻塞主线程(拦截器本身运行在 OkHttp 的线程池)。

4.4 Paging3(与协程整合)

Paging 3 有 PagingSource 返回 PagingData,结合 Pager 和 flow 使用,推荐与 lifecycleScope 一起收集。

5. 实战:ViewModel → Repository → Retrofit + Room(完整示例)

下面给出一个合理的分层示例(简化版),展示如何在协程中切 IO/Main、如何处理异常与缓存到 Room。

Gradle(关键依赖)

(见第2节)

Repository(数据层)
class UserRepository(
private val api: ApiService,
private val userDao: UserDao
) {
// 1) 直接暴露本地 Flow(Room)
val usersFlow: Flow<List<User>> = userDao.observeAll()
.flowOn(Dispatchers.IO) // 上游读取 DB 在 IO

// 2) 同步刷新:从网络拉取并存本地
suspend fun refreshFromNetwork(): Result<Unit> {
return try {
val remote = withContext(Dispatchers.IO) {
api.getUsers() // network in IO
}
withContext(Dispatchers.IO) {
userDao.insertAll(remote) // DB write in IO
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}

// 3) 安全封装模板(可复用)
suspend fun <T> safeApiCall(block: suspend () -> T): Result<T> {
return try {
val res = withContext(Dispatchers.IO) { block() }
Result.success(res)
} catch (e: Exception) {
Result.failure(e)
}
}
}

ViewModel(逻辑层)
class UserViewModel(
private val repo: UserRepository
) : ViewModel() {

// StateFlow 封装 UI 状态
private val _uiState = MutableStateFlow<List<User>>(emptyList())
val uiState: StateFlow<List<User>> = _uiState.asStateFlow()

init {
// 收集本地 DB 的 Flow 并更新 UI(收集在 Main)
viewModelScope.launch {
repo.usersFlow.collect { users ->
_uiState.value = users
}
}
}

// 手动刷新网络
fun refresh() {
viewModelScope.launch {
// 如果不想阻塞主线程,可在 launch(Dispatchers.IO) 直接做
val res = repo.safeApiCall { api.getUsers() }
res.onSuccess {
// 如果 safeApiCall 已在 IO 写 DB, 不需要再切线程
// UI 更新通过 usersFlow 自动触发
}.onFailure {
// 在 Main 处理错误提示
}
}
}
}

Activity / Fragment(UI 层)
class UserFragment : Fragment(R.layout.fragment_user) {
private val vm: UserViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launchWhenStarted {
vm.uiState.collect { users ->
// 这里运行在 Main,可以直接更新 RecyclerView
adapter.submitList(users)
}
}

refreshButton.setOnClickListener {
vm.refresh()
}
}
}


几点说明:

usersFlow.flowOn(Dispatchers.IO) 把 DB 读取放到 IO。

collect 在 lifecycleScope.launchWhenStarted {} 的上下文(Main)运行,因此无需显式切 Main 来更新 UI。

所有网络/DB 写操作都放到 IO(withContext(Dispatchers.IO) 或 launch 参数)。

6. 异常处理、取消、Supervisor、结构化并发、性能优化与常见坑
6.1 异常处理

父协程遇到未捕获异常会取消其子协程(结构化并发)。

使用 CoroutineExceptionHandler 只能捕获协程的顶层异常(launch 启动的协程),对 async 的异常需在 await() 时处理。

示例:

val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Caught $e")
}
viewModelScope.launch(handler) {
// any uncaught exception here will be handled
}


对于 async:

val deferred = viewModelScope.async { throw RuntimeException("err") }
try {
deferred.await()
} catch (e: Exception) {
// 处理
}

6.2 取消(Cancellation)

协程是协作式取消:suspend 函数需检查取消点(例如 delay, withContext, suspendCancellableCoroutine 等会响应取消)。

若在 while(true) 中做工作,需手动检查 isActive:

import kotlinx.coroutines.isActive

viewModelScope.launch {
while (isActive) {
// do work
}
}

6.3 SupervisorJob(隔离子失败)

默认 Job:子协程失败会取消父协程并传播取消。若希望子失败不影响其他子协程使用 SupervisorJob()。

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

6.4 性能优化

避免创建大量短生命周期 CoroutineScope。尽量复用 viewModelScope、lifecycleScope 或自定义长寿命 scope。

IO 操作尽量用 Dispatchers.IO,计算密集用 Default。

避免在生产端(flow 的 emit)做太重的工作:把重计算放到 map + flowOn(Dispatchers.Default)。

对并发量控制:对于大量并发请求,限制同时并发的数量(通过 Semaphore 或 Dispatcher 限制线程池)。

6.5 常见坑与纠正

不要在 suspend 函数里混用回调(未转换为 suspend),否则可能造成线程混乱。用 suspendCancellableCoroutine 把回调桥接为 suspend。

不要把所有逻辑都放在 Main scope:在 Main 中包含大量 withContext(Dispatchers.IO) 可以,但更清晰的写法是 launch(Dispatchers.IO) 来执行后台任务并在结束时切到 Main 更新 UI。

不要滥用 GlobalScope:会造成泄漏与生命周期管理困难。

async 未 await:可能会导致异常丢失(未触发时不会抛出)。

7. 最佳实践清单

使用结构化并发(viewModelScope/lifecycleScope),不要用 GlobalScope。

明确职责分层:UI(Fragment/Activity)只做展示,ViewModel 管理 UI 状态,Repository 负责数据源(网络/DB)。

IO 放 Dispatchers.IO;计算放 Default;UI 放 Main。

用 withContext 做显式切线程,更可读、易测试。

网络使用 suspend 的 Retrofit 接口;DB 使用 Room 的 suspend/Flow。

用 Flow + StateFlow 管理状态流(单向数据流)。

使用 safeApi 封装统一的错误处理与超时策略(withTimeout)。

对并发操作使用 async + await,但在发生失败时确保正确捕获异常。

使用 CoroutineExceptionHandler 处理顶层异常;对于 async 的异常在 await 时处理。

在需要子协程相互隔离失败时使用 SupervisorJob。

避免阻塞线程(Thread.sleep 等),使用 delay 与挂起函数。

测试时用 TestCoroutineDispatcher / runBlockingTest(或新版的 TestDispatcher)。

附:快速参考代码片段(最精简版)
// Retrofit service
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}

// Repository
class Repo(val api: ApiService, val dao: UserDao) {
val usersFlow = dao.observeAll().flowOn(Dispatchers.IO)
suspend fun refresh() = withContext(Dispatchers.IO) {
val list = api.getUsers()
dao.insertAll(list)
}
}

// ViewModel
class VM(val repo: Repo): ViewModel() {
val ui = repo.usersFlow.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun refresh() = viewModelScope.launch { repo.refresh() }
}

// Fragment
lifecycleScope.launchWhenStarted {
vm.ui.collect { adapter.submitList(it) }
}