协程(Coroutine)基础:协程概念、GlobalScope与lifecycleScope、suspend函数、withContext切换线程
好,咱们今天来聊聊协程。说实话,我刚接触Kotlin协程那会儿,心里是有点抵触的——毕竟Java线程池用了那么多年,也挺顺手。但真正用上协程之后,我只能说:真香。它不是什么黑魔法,说白了就是一套更优雅的异步任务管理方案。
一、协程到底是什么?
协程,你可以把它理解成「轻量级的线程」。线程是操作系统调度的,而协程是编译器帮你调度的。你想想看,线程的创建和切换开销有多大?而协程几乎零开销,一个进程里开几千个协程都没问题。
我在项目中遇到过这样一个场景:需要同时请求十几个接口,每个接口之间还有依赖关系。如果用线程池,回调嵌套能让你怀疑人生。但用协程,代码写出来就像同步调用一样清晰。
核心要点:协程不是线程,它运行在线程之上。协程可以挂起(suspend),挂起时不占用线程资源,恢复时再继续执行。
二、GlobalScope 与 lifecycleScope
这两个Scope,我刚开始用的时候也踩过坑。先看代码:
// 全局作用域 —— 慎用!
GlobalScope.launch {
// 这个协程的生命周期跟应用一样长
// 如果Activity销毁了,它还在跑
}
// 生命周期感知作用域 —— 推荐
lifecycleScope.launch {
// 这个协程会跟随Activity/Fragment的生命周期
// 页面销毁时自动取消
}
GlobalScope,说白了就是全局的。你启动一个协程,它就跟你的App共存亡。我曾经在一个项目里用GlobalScope做网络请求,结果用户退出页面了,请求还在后台跑,回调时更新UI直接崩溃。嗯,血的教训。
lifecycleScope 就聪明多了。它是Android Lifecycle库提供的,能感知Activity或Fragment的生命周期。页面销毁时,它自动取消所有协程。我个人习惯:只要是在UI层,一律用lifecycleScope。
警告:千万不要在Activity/Fragment中使用GlobalScope!它会导致内存泄漏和空指针异常。我曾经在一个线上bug里排查了半天,最后发现就是GlobalScope惹的祸。
三、suspend 函数 —— 协程的基石
suspend函数,就是可以被挂起的函数。它不会阻塞线程,而是挂起协程。等结果准备好了,再恢复执行。
// 定义一个suspend函数
suspend fun fetchUserData(): User {
// 模拟网络请求
delay(1000) // 挂起1秒,不阻塞线程
return User("张三", 28)
}
// 调用时必须在协程中
lifecycleScope.launch {
val user = fetchUserData() // 看起来像同步调用
textView.text = user.name // 更新UI
}
你注意看,fetchUserData() 这个函数前面有个 suspend 关键字。它告诉编译器:这个函数可能会挂起。调用它的地方,也必须在协程里或者另一个suspend函数里。
我刚开始写suspend函数时,总想着在里面开线程。其实完全没必要——suspend函数本身就是异步的。你只需要用 withContext 或者 delay 这些挂起函数,协程框架会自动帮你管理线程。
小技巧:suspend函数里可以调用其他suspend函数,但不能直接调用普通阻塞函数(比如Thread.sleep())。如果非要调用,用 withContext(Dispatchers.IO) 包一下。
四、withContext 切换线程
这个是我用得最多的API。它可以在不改变代码结构的前提下,轻松切换线程。
lifecycleScope.launch {
// 默认在主线程(Dispatchers.Main)
val result = withContext(Dispatchers.IO) {
// 切换到IO线程做耗时操作
// 比如读写数据库、网络请求
doHeavyWork()
}
// 自动切回主线程
textView.text = result
}
你看,代码逻辑是顺序执行的,但实际执行时,withContext 里的代码跑在IO线程,外面的代码跑在主线程。这就是协程的魅力——用同步的方式写异步代码。
我记得有一次优化一个列表加载功能。原来用RxJava,各种subscribeOn、observeOn,代码又长又绕。改成协程后,一个 withContext 搞定,代码量减少了一半,可读性还提升了。
| Dispatcher | 用途 | 线程 |
|---|---|---|
| Dispatchers.Main | UI操作、更新界面 | 主线程 |
| Dispatchers.IO | 网络请求、文件读写、数据库 | IO线程池 |
| Dispatchers.Default | CPU密集型任务(排序、解析) | CPU线程池 |
| Dispatchers.Unconfined | 不指定线程(极少用) | 当前线程 |
注意:不要在 withContext(Dispatchers.Main) 里做耗时操作!它跑在主线程,会卡UI。我曾经见过有人把大文件解析放在Main里,结果界面直接卡死5秒钟。
五、知识体系总览
下面这张图,是我自己总结的协程基础结构。你看一眼,就能把今天讲的内容串起来:
这张图把今天的内容串起来了。你看,协程是中心,左边是作用域(Scope),中间是挂起函数(suspend),右边是线程切换(withContext)。三者配合,就能写出既高效又安全的异步代码。
六、避坑指南
最后,分享几个我踩过的坑:
- 不要在协程里直接更新UI —— 除非你确定当前在Main线程。用
withContext(Dispatchers.Main)包一下最保险。 - 记得处理异常 —— 协程里的异常不会自动抛到外面。用
try-catch或者CoroutineExceptionHandler捕获。 - 不要滥用GlobalScope —— 我见过有人整个项目都用GlobalScope,结果内存泄漏查都查不完。能用lifecycleScope就用lifecycleScope。
- suspend函数里别用Thread.sleep() —— 它会阻塞线程,破坏协程的轻量特性。用
delay()代替。
好了,协程基础就聊到这儿。这些东西看起来简单,但用好了,你的代码质量会上一个台阶。下次咱们再深入聊聊Flow,那才是协程的真正威力所在。