协程入门:协程的概念与优势、GlobalScope与runBlocking、suspend函数、launch与async、Job与Deferred
各位同学,欢迎来到协程的世界。说实话,我刚开始接触Android开发那会儿,处理异步任务全靠回调嵌套回调,代码写出来跟个金字塔似的,维护起来简直想哭。后来Kotlin协程出现了,我第一次用的时候心里还在嘀咕:这玩意儿不就是个线程池封装吗?结果用了一个星期,真香。
协程,说白了就是一套帮你管理异步任务的框架。它不是线程,但跑在线程上。你可以把它理解成一种轻量级的线程——轻量到什么程度?一个线程能跑成千上万个协程,而且切换成本极低。嗯,这里要注意,协程本身并不等于并发,它只是让异步代码写起来像同步代码一样自然。
核心要点:协程是Kotlin提供的异步编程解决方案,它让回调地狱变成了顺序代码。你想想看,以前写网络请求要回调里套回调,现在用协程,直接一行suspend函数搞定。
协程的优势
我在项目中遇到过不少场景,比如同时请求三个接口,等全部返回后再更新UI。用传统方式你得写CountDownLatch或者RxJava的zip操作符,代码量不小。用协程呢?一个async就解决了。
- 轻量级:一个协程就是一个状态机,不占用额外线程资源。我测试过,开10万个协程毫无压力,换成线程早OOM了。
- 结构化并发:协程有父子关系,父协程取消,子协程自动取消。这比手动管理线程安全多了。
- 代码可读性高:用协程写的异步代码,读起来就像同步代码一样。没有回调,没有嵌套。
- 异常处理友好:try-catch就能捕获协程里的异常,不用像RxJava那样写一堆onError。
GlobalScope 与 runBlocking
先说说GlobalScope。这玩意儿我建议你少用,甚至别用。为什么?因为它是一个全局作用域,协程的生命周期跟整个应用绑定。你在Activity里用GlobalScope启动一个协程,Activity销毁了协程还在跑,这就容易出内存泄漏。我曾经在一个老项目里看到到处都是GlobalScope.launch,结果内存泄漏查了半天,最后全改成lifecycleScope了。
// 不推荐:GlobalScope 启动的协程生命周期与应用一致
GlobalScope.launch {
delay(1000)
println("这个协程可能泄漏")
}
// 推荐:使用 lifecycleScope 或 viewModelScope
lifecycleScope.launch {
delay(1000)
println("安全,Activity销毁协程自动取消")
}
再来看runBlocking。这名字起得很直白——它会阻塞当前线程直到协程执行完毕。一般用在main函数或者测试代码里。你想想看,在Android主线程里用runBlocking?那画面太美我不敢看,直接ANR警告。
// 通常在 main 函数或测试中使用
fun main() = runBlocking {
launch {
delay(1000)
println("World")
}
println("Hello")
// 输出顺序:Hello -> (1秒后) World
}
警告:千万不要在Android主线程中使用runBlocking!它会阻塞UI线程,导致应用无响应。我曾经见过有人这么写,结果被测试小姐姐追着骂了一下午。
suspend 函数
suspend是协程的核心关键字。一个函数加上suspend,就变成了可挂起的函数。什么叫挂起?说白了就是协程执行到这个函数时,可以暂停下来,等函数执行完再继续往下走。而且这个暂停不会阻塞线程,线程可以去干别的事。
我个人习惯把suspend函数想象成一个带暂停按钮的播放器。你点暂停,播放器停了,但电源没断。等再点播放,从暂停的地方继续放。协程的挂起也是这个道理。
// suspend 函数示例
suspend fun fetchUserData(): User {
// 模拟网络请求
delay(2000) // delay 本身就是一个 suspend 函数
return User("张三", 25)
}
// 调用 suspend 函数必须在协程作用域内
lifecycleScope.launch {
val user = fetchUserData() // 这里会挂起2秒,但不会阻塞主线程
textView.text = user.name
}
小技巧:suspend函数只能在协程作用域内或者其他suspend函数中调用。如果你在普通函数里调用suspend函数,编译器会报错。嗯,这个坑我刚开始也踩过。
launch 与 async
launch和async是启动协程的两种方式。launch返回一个Job,不关心返回值;async返回一个Deferred,可以通过await()获取返回值。怎么选?很简单:需要返回值就用async,不需要就用launch。
// launch:不关心返回值
lifecycleScope.launch {
// 执行一些操作,不需要返回结果
updateUI()
}
// async:需要返回值
lifecycleScope.launch {
val deferred = async {
fetchDataFromNetwork()
}
val result = deferred.await() // 等待结果
showResult(result)
}
我在项目中经常用async做并行请求。比如同时请求用户信息和商品列表,两个接口没有依赖关系,就可以并行执行,等两个都返回后再一起处理。
lifecycleScope.launch {
val userDeferred = async { apiService.getUserInfo() }
val productDeferred = async { apiService.getProductList() }
// 两个请求并行执行,这里等待两个都完成
val user = userDeferred.await()
val products = productDeferred.await()
// 更新UI
showUserInfo(user)
showProducts(products)
}
Job 与 Deferred
Job是协程的句柄,你可以通过它来控制协程的生命周期。比如取消协程、等待协程完成。Deferred是Job的子接口,多了一个await()方法用来获取返回值。
| 特性 | Job | Deferred |
|---|---|---|
| 返回值 | 无 | 有(通过await获取) |
| 启动方式 | launch | async |
| 取消 | job.cancel() | deferred.cancel() |
| 等待完成 | job.join() | deferred.await() |
// Job 的使用示例
lifecycleScope.launch {
val job = launch {
repeat(10) {
delay(500)
println("执行第 $it 次")
}
}
delay(2000)
job.cancel() // 2秒后取消协程
println("协程已取消")
}
// Deferred 的使用示例
lifecycleScope.launch {
val deferred = async {
delay(1000)
"Hello from async"
}
val result = deferred.await() // 等待并获取结果
println(result) // 输出:Hello from async
}
重要提醒:Job和Deferred都支持结构化并发。如果父协程被取消,所有子协程也会被自动取消。这个特性在Android开发中特别有用,比如页面销毁时自动取消所有网络请求。
好了,协程入门的内容就这些。说实话,协程刚出来的时候我也觉得有点绕,但用习惯了真的回不去。你想想看,以前写异步代码要各种回调、线程切换,现在一个suspend函数加一个launch就搞定了。下一节我们会深入协程的调度器与上下文,到时候再聊更多实战技巧。
公众号:蓝海资料掘金营,微信deep3321