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) } }
|