作者:J船长
一、协程协程,恢复挂起,让异步像同步
重要的说三遍
-
协程协程,恢复挂起,让异步像同步
-
协程协程,恢复挂起,让异步像同步
-
协程协程,恢复挂起,让异步像同步
经常有这么一种感觉,读完一本小说,内容基本终将都是遗忘,能记住一句话很多时候都实属难得。
那么如果说起协程只能留个一个印象,那么可以是:
协程的核心是挂起(suspend)和恢复(resume),最经典的应用就是让几个异步方法能以类似同步的方式排队去执行。
爱与和平,比喻与爱
想象一下,你正在看一部非常引人入胜的电影,但是突然你想上厕所。你难道会错过电影中精彩的部分吗?当然不会。你会“暂停”电影,上完厕所再“恢复”播放。这就是协程的核心概念:挂起与恢复。
你可以把协程想象成一个大厨,他正在烹饪多道菜品。当一个菜品需要等待一段时间(比如煮沸或烘烤)时,大厨不会傻傻地等待,而是转向另一个任务。这就是协程的核心思想:挂起与恢复。
-
协程有两个基本操作:挂起(suspend)和恢复(resume)。
-
挂起不是停止执行,而是把控制权交还给调用者,直到我们准备好恢复执行。
-
恢复就是我们准备好继续执行时,从上次暂停的地方恢复执行。
什么时候,需要用到协程?
协程,轻量级的线程管理工具。 什么时候用到? ——
执行异步任务
和
管理并发操作
-
异步、并发,用协程
-
异步、并发,用协程
-
异步、并发,用协程
- 处理阻塞操作:如果你需要在主线程上执行可能会阻塞的操作,例如网络请求或数据库查询,那么你应该创建一个协程。在协程中执行这些操作可以避免阻塞主线程,保持应用的响应性。
- 并发执行任务:如果你需要同时执行多个任务,并在它们全部完成后处理结果,你可以创建多个协程,并使用 async 函数或 withContext 函数来并发执行这些任务。
- 实现复杂的并发逻辑:协程提供了一些高级的并发原语,如通道(Channel)和流(Flow),你可以使用它们来实现更复杂的并发逻辑。
- 优化资源使用:协程是一种轻量级的线程,你可以创建大量的协程而不需要担心线程资源的开销。如果你需要在应用中执行大量的异步任务,那么使用协程可能会比使用线程更高效。
- 在 Android 开发中,你还可以使用协程来配合 LiveData 或 Flow 实现数据的异步加载和更新,以及配合 Room 数据库库进行异步查询操作等。
以上这些场景都是创建协程的常见情况。使用协程可以让异步代码的写法更接近同步代码,更加直观易读,同时也让并发操作的控制更为简单。
最经典的场景
最经典的,无非就是,多个网络请求,一起执行,让这些本来都是异步的代码,像同步一样地执行。
二、 协程的依赖
安卓项目中使用协程,先添加依赖
build.gradle
引入一下
// 协程依赖包
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
// 协程Android支持库
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
三、先从最经典的场景说起
假设,有一个页面,需要用Retrofit,请求2个接口,当且仅当两个接口都请求成功时,汇总数据返回,更新UI。
其中,一个接口请求用户详情,一个接口请求文章数据,一个类用于汇总数据。
定义我们的数据类和API接口:
-
接口方法被标记为
suspend
,这意味着它们是挂起函数,可以在协程中异步执行
// 用户详细信息数据类
data class UserDetails(/* ... */)
// 用户文章数据类
data class Post(/* ... */)
// 用户详细信息和文章的数据类
data class UserDetailsAndPosts(val userDetails: UserDetails, val userPosts: List<Post>)
// API接口定义
interface ApiService {
@GET("user/{userId}")
suspend fun fetchUserDetails(@Path("userId") userId: String): UserDetails
@GET("posts/{userId}")
suspend fun fetchUserPosts(@Path("userId") userId: String): List<Post>
}
创建Retrofit实例
-
我们使用了
CoroutineCallAdapterFactory
,它允许我们在 Retrofit 中使用协程。
val retrofit = Retrofit.Builder()
.baseUrl("https://your-api-url.com/") // API的基础URL
.addConverterFactory(GsonConverterFactory.create()) // 使用Gson转换器
.addCallAdapterFactory(CoroutineCallAdapterFactory()) // 使用协程适配器
.build()
val apiService = retrofit.create(ApiService::class.java) // 创建API服务实例
ViewModel中处理异步请求
-
使用了
viewModelScope
来启动协程,这意味着如果 ViewModel 被清除,所有的协程也会被取消,从而避免了内存泄露。 -
用
async
函数。
async
函数会启动一个新的协程并返回一个
Deferred
对象,这个对象代表了一个未完成的协程。我们可以调用
Deferred
对象的
await
方法来等待协程完成并获取结果。
class UserViewModel(private val apiService: ApiService) : ViewModel() {
val userDetailsAndPostsLiveData = MutableLiveData<UserDetailsAndPosts>()
fun fetchUserDetailsAndPosts(userId: String) {
// 在ViewModel的协程范围内启动协程
viewModelScope.launch {
val userDetailsDeferred = async {
try {
apiService.fetchUserDetails(userId) // 获取用户详细信息
} catch (e: Exception) {
// 处理 fetchUserDetails 的异常
null
}
}
val userPostsDeferred = async {
try {
apiService.fetchUserPosts(userId) // 获取用户文章列表
} catch (e: Exception) {
// 处理 fetchUserPosts 的异常
null
}
}
// 等待两个请求完成并获取结果
val userDetails = userDetailsDeferred.await()
val userPosts = userPostsDeferred.await()
// 如果两个请求都成功,创建 UserDetailsAndPosts 实例并更新 LiveData
if(userDetails != null && userPosts != null){
val userDetailsAndPosts = UserDetailsAndPosts(userDetails, userPosts)
userDetailsAndPostsLiveData.value = userDetailsAndPosts
}
}
}
}
在Activity中使用
class UserActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 获取ViewModel实例
viewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
//
上面代码,大概看个印象,可以看下下面的分析,也可以直接跳到第四大点。
上面的代码,到底讲了什么
我们对这个使用 Kotlin 协程的网络请求例子做一个分析和总结。
-
API接口定义
:在这个例子中,我们首先定义了 API 接口。使用 Retrofit 库,我们可以通过创建接口并使用注解来定义网络请求。最重要的是,我们的接口方法被标记为
suspend
,这意味着它们是挂起函数,可以在协程中异步执行。 -
创建Retrofit实例
:我们创建了 Retrofit 客户端实例,并通过调用
create
方法创建 API 接口的实例。注意我们使用了
CoroutineCallAdapterFactory
,它允许我们在 Retrofit 中使用协程。 -
在ViewModel中处理异步请求
:我们在 ViewModel 中定义了一个 LiveData 对象来保存用户详细信息和文章的数据,然后创建了一个处理异步请求的方法。在这个方法中,我们使用了
viewModelScope
来启动协程,这意味着如果 ViewModel 被清除,所有的协程也会被取消,从而避免了内存泄露。为了并行执行两个网络请求,我们使用
async
函数。
async
函数会启动一个新的协程并返回一个
Deferred
对象,这个对象代表了一个未完成的协程。我们可以调用
Deferred
对象的
await
方法来等待协程完成并获取结果。需要注意的是,这些协程都在
viewModelScope
的范围内,因此它们的生命周期受 ViewModel 的控制。对于每个网络请求,我们使用
try-catch
块来处理可能的异常。如果请求成功,我们将结果保存到 LiveData 对象中;如果请求失败,我们可以根据需要处理异常。 -
在Activity中使用ViewModel
:在 Activity 中,我们获取 ViewModel 的实例,然后观察 LiveData 的变化来更新 UI。我们还调用了
fetchUserDetailsAndPosts
方法来启动网络请求。
这个例子展示了如何使用 Kotlin 协程来处理并行的异步任务,并通过 LiveData 对象将结果返回到 UI。Kotlin 协程提供了一种简洁的方式来管理异步操作,避免了回调地狱,使代码更易于阅读和维护。此外,通过正确使用
viewModelScope
,我们可以确保在 ViewModel 被清除时取消所有的协程,从而避免了内存泄露。
上面这通解释分析,你也可以略过,下面开始正式比较系统地,说一说协程
四、协程,正式开打
四、 协程离不开的4个东西
-
suspend function : 即挂起函数
-
CoroutineScope : 即协程作用域
-
CoroutineContext: 即协程上下文
-
CoroutineBuilder: 即协程构建器
巴拉巴拉一堆名词。
写个最简单的协程代码,然后圈出来看一下。
了然了然一下
最简单的协程代码
fun log(msg: Any?) = println("ThreadName: [${Thread.currentThread().name}] threadId:[${Thread.currentThread().id}] $msg")
fun main() {
GlobalScope.launch(context = Dispatchers.IO) {
//延时一秒
delay(1000)
log("launch")
}
//主动休眠两秒,防止 JVM 过快退出
Thread.sleep(2000)
log("end")
}
输出:
ThreadName: [DefaultDispatcher-worker-1] threadId:[11] launch
ThreadName: [main] threadId:[1] end
那么,谁是 挂起函数function ?作用域Scope? 上下文Context? 构建器Build?
Emmm,F4的第一次见面,结束。
五、协程的作用域 CoroutineScope
CoroutineScope作用域这玩意,必须先讲,不然如果是刚入门的同学,可能会因为没先了解这个东西,整的云里雾里。GlobalScope这个作用域,它不会阻止 JVM 结束运行,这意味着!!!!!很可能JVM都结束了,你的协程还在运行,然后问题就来了,你在代码里面写了一些输出语句,然后你死等,就等不到你的输出语句被打印出来。然后一脸懵逼,二脸懵逼,懵山懵海。尽量别用它
几个作用域
-
GlobalScope
尽量避免使用
GlobalScope
-
CoroutineScope
常用
-
MainScope
-
runBlocking
-
viewModelScope
安卓专用,配合viewModel
常用
-
lifecycleScope
安卓专用,配合lifecycle
常用
-
GlobalScope
:全局作用域,生命周期和应用一样长。虽然可以在任何地方启动协程,但在实际开发中尽量避免使用,以防止协程无法及时被取消造成的内存泄漏。 -
CoroutineScope
:一个通用的协程作用域接口,可以自定义作用域的上下文,比如你可以创建一个
CoroutineScope
的实例并指定其生命周期。 -
MainScope
:是 CoroutineScope 的一个实例,它在主线程上创建协程。通常用在 Android UI 线程的操作,需要手动进行取消操作(OnDestory)以防止内存泄漏。 -
runBlocking
:这是一个特殊的协程作用域,会阻塞当前线程并等待协程执行完毕。通常在测试和主函数中使用,开发中避免使用,因为它会阻塞线程,可能导致应用无响应。 -
viewModelScope
:这是 Android 架构组件提供的预定义作用域,专门为 ViewModel 设计,当 ViewModel 被清除时,
viewModelScope
中的所有协程也会自动被取消。在 Android 开发中,这是最常使用的作用域之一。 -
lifecycleScope
:同样由 Android 架构组件提供,用于
LifecycleOwner
(如 Activity 或 Fragment)。当
LifecycleOwner
的生命周期结束时,
lifecycleScope
中的所有协程会自动取消。
作用域的开发场景代码
典型的应用场景
GlobalScope: 几乎不用
GlobalScope在 Android 开发中,
GlobalScope 的使用场景较少
,主要应用在应用程序级别的后台任务中。例如,可能需要在整个应用程序生命周期内持续监听网络连接状态的变化,这时候就可以使用 GlobalScope。但这需要谨慎处理,否则可能会导致内存泄漏。
GlobalScope.launch {
// 这里我们在全局作用域启动一个新的协程,它会在整个应用程序生命周期内持续运行
// 这个例子中,我们假设需要在应用程序生命周期内持续监听网络连接状态的变化
while (isActive) { // 在协程仍处于活动状态时持续执行
val isConnected = checkNetworkConnection() // 假设这是一个检查网络连接的挂起函数
if (!isConnected) {
notifyUser() // 如果网络断开,通知用户
}
delay(5000) // 每 5 秒检查一次网络连接
}
}
CoroutineScope: 常用
CoroutineScope自定义作用域,用于创建一个具有特定生命周期和调度器的协程作用域,例如,可以用于处理一些长时间运行的后台任务。
val job = Job()
val dispatcher = Dispatchers.IO
val customScope = CoroutineScope(job + dispatcher)
customScope.launch {
// 在自定义的协程作用域中启动一个新的协程。
// 这个协程的生命周期与 `job` 相关联,当我们调用 `job.cancel()` 时,这个协程会被取消。
// 这个协程会在 IO 调度器上运行,这适合执行 IO 密集型的任务,如网络请求或磁盘读写。
val data = fetchDataFromNetwork() // 假设这是一个挂起函数,用于从网络加载数据
processData(data) // 处理数据
}
MainScope
MainScope:MainScope 在 Android 开发中通常用于在
主线程上执行一些短暂的任务
,这些任务
需要在用户界面上显示结果
。例如,我们可能需要从
网络上下载一张图片
,然后在主线程上
更新用户界面
。不过,需要注意的是,与 viewModelScope 和 lifecycleScope 不同,
MainScope
不会自动取消协程,我们
需要在适当的时候手动取消协程
,例如在 Activity 的
onDestroy
方法中,
否则可能会导致内存泄漏
。
class MainActivity : AppCompatActivity() {
private val mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainScope.launch {
// 在主线程上获取数据
val data = fetchDataFromNetwork() // 假设这是一个挂起函数,用于从网络加载数据
// 在主线程上更新 UI
textView.text = data
}
}
override fun onDestroy() {
super.onDestroy()
// 当 Activity 销毁时,取消所有的协程以避免内存泄漏
mainScope.cancel()
}
}
viewModelScope 常用
viewModelScope:在 ViewModel 中运行协程,当 ViewModel 清理时,所有的协程都会被自动取消。适合用于触发数据加载等操作。
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
// 在 ViewModel 的作用域中启动一个新的协程
// 当 ViewModel 被清理时,这个协程会被自动取消
val data = fetchDataFromNetwork() // 假设这是一个挂起函数,用于从网络加载数据
_data.value = data // 更新 LiveData
}
}
}
lifecycleScope 常用
lifecycleScope:在生命周期拥有者(如 Activity 或 Fragment)中运行协程,当生命周期结束时,所有的协程都会被自动取消。适合在 UI 控制器中触发数据加载等操作。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
// 在 Activity 的生命周期作用域中启动一个新的协程
// 当 Activity 销毁时,这个协程会被自动取消
val data = fetchDataFromNetwork() // 假设这是一个挂起函数,用于从网络加载数据
textView.text = data // 更新 UI
}
}
}
runBlocking
runBlocking:在当前线程阻塞并启动新协程。在 Android 中,runBlocking 主要用于测试中,因为在真实的 Android 设备上,我们通常避免在主线程上阻塞。
@Test
fun testCoroutine() = runBlocking {
// 在测试中,我们可以使用 `runBlocking` 在当前线程阻塞并启动新协程
// 这样可以确保在测试方法返回之前,协程内的所有操作都已完成
val data = fetchDataFromNetwork() // 假设这是一个挂起函数,用于从网络加载数据
assertEquals("expected data", data)
}
其实,对于开发初步使用
看到这里,基本也就够了。
当然,但是,接着说,也是可以的!
六、协程的构造器 CoroutineBuilder
其实吧,这个东西,有点模糊。官方只说了launch和async是CoroutineBuilder。但是对于 runBlocking 和 coroutineScope,它们同时具有作用域和构造器的特性,但是我们这里,不把他们当做构造器,只认为 launch 和 async 是构造器。
协程的两个启动方法
-
launch
不关心返回结果 Job -
async
可以返回结果 Deferred
launch
和
async
的对比
launch
async
launch
和
async
都是用来创建新协程的构造器。它们的主要区别在于
返回值
和
目的
。
-
launch
用来启动一个新协程,这个协程不会返回任何结果(而是返回一个
Job
对象)。它的主要用途是执行一些并发的操作,这些操作自身就是它们的目的,我们并不关心它们的返回值。我们可以使用这个返回的
Job
对象来控制这个协程,比如取消它。 -
async
用来启动一个新协程,并且这个协程可以返回一个结果。
async
返回一个
Deferred
对象,这是一个特殊的
Job
,我们可以使用
await()
方法来获取这个协程的结果。如果这个协程还没有完成,
await()
方法会挂起当前协程,直到这个协程完成。
Job
和
Deferred
的对比
Job
Deferred
Job
和
Deferred
都代表一个协程的生命周期和状态。它们的主要区别在于,
Deferred
可以有一个结果。
-
Job
表示一个协程的生命周期。我们可以调用
Job
的
cancel()
方法来取消这个协程。
Job
还有一些其他的方法和属性,用来查询协程的状态,比如
isActive
、
isCompleted
和
isCancelled
。 -
Deferred
是
Job
的一个子接口,它增加了一个
await()
方法,用来获取协程的结果。当你调用
await()
方法时,如果这个协程还没有完成,当前协程会被挂起,直到这个协程完成。因此,
Deferred
是一个可以有结果的
Job
。
launch
和
async
代码演示
launch
async
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
private val job = Job() // 用来管理协程的生命周期
private val scope = CoroutineScope(Dispatchers.Main + job) // 在主线程上创建协程作用域
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// =========== 以下演示 launch
// 使用 launch 启动一个新协程,这个协程不会返回任何结果。主要用途是执行一些并发的操作
scope.launch {
try {
// 从网络获取用户数据
// 这里的 fetchUserFromNetwork 是一个 suspend 函数,也就是一个挂起函数,它可以在协程中运行
val user = fetchUserFromNetwork()
// 在数据库中更新用户数据
updateUserInDatabase(user)
} catch (e: Exception) {
// 处理可能发生的异常,例如网络错误或数据库错误
handleException(e)
}
}
// =========== 以下演示 async
// 使用 async 启动一个新协程,并且这个协程可以返回一个结果。
// async 返回一个 Deferred 对象,这是一个特殊的 Job,我们可以使用 await() 方法来获取这个协程的结果。
val deferred: Deferred<User> = scope.async {
fetchUserFromNetwork()
}
// 使用 await 获取协程的结果
// 当你调用 await() 方法时,如果这个协程还没有完成,当前协程会被挂起,直到这个协程完成。
scope.launch {
try {
val user = deferred.await()
updateUserInDatabase(user)
} catch (e: Exception) {
handleException(e)
}
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel() // 取消所有的协程
}
// 下面是一些模拟的函数
// suspend 关键字表明这是一个挂起函数。挂起函数可以在协程中运行,并且可以被挂起,而不会阻塞线程。
private suspend fun fetchUserFromNetwork(): User {
delay(1000) // 模拟网络请求
return User() // 返回一个用户对象
}
private suspend fun updateUserInDatabase(user: User) {
delay(500) // 模拟数据库操作
}
private fun handleException(e: Exception) {
// 在这里处理异常
}
}
class User {
// 用户类
}
这备注,可以说是相当详细了吧。
关于协程的构造器,可以了
七、协程的上下文
在 Android 开发中,以下几个协程上下文是比较常用的:
Dispatchers.Main
1.
Dispatchers.Main
:在主线程上执行协程代码,用于处理 UI 相关的操作,例如更新界面、响应用户交互等。在 Android 中,由于 UI 操作必须在主线程上执行,因此使用
Dispatchers.Main
是非常常见的。(由于协程在主线程中执行,不应该在该上下文中进行耗时操作,以免阻塞主线程导致应用无响应。)
Dispatchers.IO
2.
Dispatchers.IO
:用于执行 I/O 相关的操作,如网络请求、文件读写等。它使用了一个线程池来运行协程代码,适用于执行长时间的阻塞操作。在进行网络请求、数据库操作或文件读写等 I/O 操作时,使用
Dispatchers.IO
可以避免阻塞主线程,提高应用程序的响应性。
Dispatchers.Default
2.
Dispatchers.Default
:用于执行计算密集型操作,如数据处理或计算。它使用了一个线程池来运行协程代码,适用于执行消耗 CPU 资源的任务。当需要进行复杂的计算或处理大量数据时,使用
Dispatchers.Default
可以将这些操作分配到后台线程,避免阻塞主线程。
除了上述常用的协程上下文,还有一些其他的上下文也可能在特定的场景中使用:
Dispatchers.Unconfined
3.
Dispatchers.Unconfined
:这个上下文不受任何特定线程的限制,协程会在恢复后恢复到任意线程上。它适用于一些无需特定线程限制的操作,但需要注意的是,由于协程在不同的线程之间切换,可能会导致上下文切换的开销。
CoroutineName
4.
CoroutineName
:这个上下文用于为协程指定一个名称,以方便调试和追踪。在复杂的协程流程中,给协程命名可以帮助我们更好地理解和跟踪协程的执行路径。
在 Android 开发中,常用的协程上下文是根据具体的需求和场景选择的。主要关注需要在主线程上执行的 UI 操作、I/O 操作和计算密集型操作,并根据需要选择适当的协程上下文。
也就是说,日常使用基本都是
Dispatchers.Main
,
Dispatchers.IO
,
Dispatchers.Default
Dispatchers.Main示例
- 适用场景:用于在主线程上执行协程任务,例如更新用户界面。
- 由于协程在主线程中执行,不应该在该上下文中进行耗时操作,以免阻塞主线程导致应用无响应。
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 启动一个协程,在主线程中更新UI
CoroutineScope(Dispatchers.Main).launch {
val result = fetchUserData()
updateUI(result)
}
}
private suspend fun fetchUserData(): String {
// 模拟耗时操作
delay(1000)
return "User Data"
}
private fun updateUI(data: String) {
textView.text = data
}
}
Dispatchers.IO:
- 适用场景:用于执行涉及 I/O 操作的协程任务,例如网络请求、文件读写等。
- 这个上下文适用于执行会阻塞线程的 I/O 操作,如网络请求、文件读写等。它会自动根据需要创建足够的线程池来处理任务。(不要在主线程中执行)
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import kotlin.coroutines.CoroutineContext
class MainActivity : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
job = Job()
launch {
val result = fetchDataFromNetwork()
updateUI(result)
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
// 在 IO 线程中执行网络请求并读取数据
private suspend fun fetchDataFromNetwork(): String = withContext(Dispatchers.IO) {
val url = URL("https://api.example.com/data")
val connection = url.openConnection()
val inputStream = connection.getInputStream()
val reader = BufferedReader(InputStreamReader(inputStream))
val stringBuilder = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
stringBuilder.append(line)
}
return@withContext stringBuilder.toString()
}
// 更新 UI 的函数
private fun updateUI(data: String) {
val textView = findViewById<TextView>(R.id.textView)
textView.text = data
}
}
Dispatchers.Default:
- 适用场景:用于执行 CPU 密集型的计算任务,例如排序、解析数据等。
- 这个上下文适用于执行消耗 CPU 资源较多的计算任务,如排序、解析数据等。它会根据需要创建足够的线程池来处理任务。
class MainActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
scope.launch(Dispatchers.Default) {
// 在 Default 线程池上执行协程中的计算操作
val result = performCalculation()
withContext(Dispatchers.Main) {
// 切换回主线程更新界面
updateUI(result)
}
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 取消协程
}
private suspend fun performCalculation(): Int {
delay(1000) // 模拟计算操作
return 42
}
private fun updateUI(result: Int) {
// 在 UI 线程上更新界面
textView.text = "Result: $result"
}
}
八、协程 恢复和挂起
挂起(suspend)和恢复(resume)是协程的两个核心操作。
简单来说:
-
挂起suspend
就是暂停当前协程的执行 -
恢复resume
就是重新开始协程的执行。
我们可以把协程想象成一部正在播放的电影。当你需要离开一会(比如接个电话),你可能会按下”暂停”按钮,这就是挂起协程。当你准备好继续观看时,你会按下”播放”按钮,这就是恢复协程。
-
挂起函数(Suspend function)是Kotlin协程中的一种特殊函数,它可以在不阻塞当前线程的情况下暂停协程的执行。Kotlin标准库中的许多函数都是挂起函数,例如
delay()
和
yield()
。 -
被
suspend
修饰之后函数就可以被 称之为 挂起函数
谁可以调用挂起函数
在 Kotlin 协程中,挂起函数只能在以下环境中被调用:
- 其他挂起函数:一个挂起函数可以调用其他的挂起函数。这意味着你可以在一个挂起函数中调用另一个挂起函数,而不需要任何特殊的语法或关键字。
-
协程构建器:挂起函数可以在协程构建器(如
launch
或
async
)中被调用。这是启动一个新的协程并在其中调用挂起函数的常见方式。 - CoroutineScope:在一个 CoroutineScope 的扩展函数中,你可以直接调用挂起函数。
挂起suspend
函数的小例子
挂起suspend
看代码吧
我们前面说过,delay()本身也是一个挂起函数
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
println("协程开始执行 - ${Thread.currentThread().name}")
delay(1000L) // 这是一个挂起函数
println("协程恢复执行 - ${Thread.currentThread().name}")
}
println("主线程继续执行 - ${Thread.currentThread().name}")
}
在上面的代码中,
launch
函数启动了一个新的协程,并在内部调用了
delay(1000L)
。这个
delay
函数就是一个挂起函数,它会暂停协程的执行1秒钟(但不会阻塞线程),然后恢复协程的执行。
这段代码的输出将是:
协程开始执行 - main
主线程继续执行 - main
协程恢复执行 - main
我们看到,输出顺序是 开始 —— 继续 ——恢复。
注意观察,虽然我们的协程在等待时,主线程并没有被阻塞,而是继续执行了下一行代码。协程的暂停并不会阻塞线程。
再来一个例子
class MainActivity : AppCompatActivity() {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
coroutineScope.launch {
Log.d("协程示例", "onCreate: 协程开始")
try {
val result = withContext(Dispatchers.IO) {
performNetworkRequest()
}
Log.d("协程示例", "onCreate: 收到网络响应: $result")
// 在这里处理网络响应
} catch (e: Exception) {
Log.e("协程示例", "onCreate: 网络请求失败", e)
// 处理网络请求失败的情况
}
}
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
suspend fun performNetworkRequest(): String {
Log.d("协程示例", "performNetworkRequest: 开始网络请求...")
delay(2000) // 模拟网络请求的延迟
return "网络响应"
}
}
打印输出:
onCreate: 协程开始
performNetworkRequest: 开始网络请求...
onCreate: 收到网络响应: 网络响应
常见的挂起函数
被 suspend 修饰的函数就是挂起函数,协程也自带一些挂起函数
协程的挂起函数有许多,比如
delay
,
join
,
await
,
withContext
,
yield
,
withTimeout
等等。这些函数的作用和特点如下:
-
delay
:延迟一段时间再继续执行协程。 -
join
:等待一个协程的完成。 -
await
:等待一个协程的结果。 -
withContext
:在特定的上下文(比如在不同的线程或调度器)中执行代码。 -
yield
:让出CPU的控制权,让其他协程有机会执行。 -
withTimeout
:设置协程的超时时间。
以下是一个例子,结合 Android 的 Activity,演示如何使用这些函数:
import kotlinx.coroutines.*
class MyActivity : AppCompatActivity() {
// 创建一个协程的上下文
private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 创建并启动一个协程
scope.launch {
val result = getDataFromNetwork() // 这个函数是一个挂起函数
// 这里可以更新UI
textView.text = result
}
}
// 从网络获取数据的挂起函数
private suspend fun getDataFromNetwork(): String {
// withContext 可以改变协程的上下文
return withContext(Dispatchers.IO) {
// 假设我们在这里使用了一个网络请求库,比如Retrofit
// 它的函数通常也是挂起函数,可以在协程中直接调用
// 这里我们简单模拟一下网络请求
delay(1000) // 模拟网络延迟
"获取到的数据"
}
}
override fun onDestroy() {
super.onDestroy()
// 当Activity销毁时,取消所有的协程以防止内存泄漏
scope.cancel()
}
}
单独演示下常用的 withContext
class MainActivity : AppCompatActivity() {
// 创建一个 CoroutineScope,使用 Dispatchers.Main 作为默认的调度器
val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 launch 开启一个协程
scope.launch {
Log.d("MainActivity", "开始协程,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
// 使用 withContext 切换到 IO 调度器
withContext(Dispatchers.IO) {
downloadDataFromNetwork() // 模拟耗时操作,比如网络请求
// 使用 withContext 再次切换到 Main 调度器
withContext(Dispatchers.Main) {
Log.d("MainActivity", "切换回 Main 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
// 在主线程更新 UI
// textView.text = "更新后的文本"
}
// 使用 withContext 切换到 Default 调度器
withContext(Dispatchers.Default) {
// 耗时计算
Log.d("MainActivity", "切换到 Default 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
}
}
}
}
// 模拟从网络下载数据的挂起函数
private suspend fun downloadDataFromNetwork(): String {
// withContext 可以改变协程的上下文
return withContext(Dispatchers.IO) {
Log.d("MainActivity", "切换到 IO 调度器,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
Log.d("MainActivity", "开始下载数据,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
delay(1000) // 模拟网络下载延迟
Log.d("MainActivity", "数据下载完成,当前线程: ${Thread.currentThread().name}") // 打印日志,显示当前线程名
"下载的数据" // 返回下载的数据
}
}
override fun onDestroy() {
super.onDestroy()
// 当 Activity 销毁时,取消所有协程
scope.cancel()
}
}
输出
开始协程,当前线程: main
切换到 IO 调度器,当前线程: DefaultDispatcher-worker-1
开始下载数据,当前线程: DefaultDispatcher-worker-1
数据下载完成,当前线程: DefaultDispatcher-worker-1
切换回 Main 调度器,当前线程: main
切换到 Default 调度器,当前线程: DefaultDispatcher-worker-1
emm,基本写到这里的话,日常开发也够用了。不写了太长了这个文章
番外篇 协程到底是不是在单线程里并发?还是不多线程?
协程可能在单线程,也可能是多线程。具体要取决于Dispatcher
协程本身并不直接与线程对应,它是一种轻量级的线程,可以在单个线程内并发执行,也可以在多个线程之间切换。一个协程可以在一个线程中启动,然后在另一个线程中暂停,然后再在另一个线程中恢复。
具体来说,
协程的运行取决于它的调度器(Dispatcher)
。调度器决定了协程在哪个线程或哪些线程上执行。比如
Dispatchers.Main
是主线程调度器,
Dispatchers.IO
是专门用于磁盘和网络 IO 读写的调度器,
Dispatchers.Default
是用于 CPU 密集型任务的调度器,
Dispatchers.Unconfined
是一个特殊的调度器,它没有特定的线程,会在当前线程立即执行协程,如果协程中有挂起点,它会在恢复时继续在其他合适的线程执行。
总的来说,
协程既可以在单线程中并发运行,也可以在多线程中运行,取决于你的需求和使用的调度器
。但是,与线程相比,协程更轻量级,可以创建成千上万个而不会对性能造成大的影响。
来例子啊
在单线程内并发运行多个协程
import kotlinx.coroutines.*
fun main() = runBlocking {
// 在主线程启动两个协程
launch {
println("协程1在 ${Thread.currentThread().name} 线程运行")
delay(1000L)
println("协程1在 ${Thread.currentThread().name} 线程结束")
}
launch {
println("协程2在 ${Thread.currentThread().name} 线程运行")
delay(1000L)
println("协程2在 ${Thread.currentThread().name} 线程结束")
}
}
这个例子中,我们在主线程中启动了两个协程,它们会并发执行,但都在同一个线程内。
在多线程之间切换协程:
import kotlinx.coroutines.*
fun main() = runBlocking {
// 在主线程启动协程
launch(Dispatchers.Main) {
println("协程在 ${Thread.currentThread().name} 线程开始")
// 使用 withContext 切换到 IO 线程
withContext(Dispatchers.IO) {
println("协程在 ${Thread.currentThread().name} 线程运行")
}
println("协程在 ${Thread.currentThread().name} 线程结束")
}
}
这个例子中,我们首先在主线程中启动了一个协程,然后使用
withContext(Dispatchers.IO)
切换到了 IO 线程,然后又回到了主线程。我们可以在日志中看到协程在不同的线程之间切换。
Kotlin+协程已经让我们在网络编程中体会到了它的过人之处。为了能让大家能够顺畅的使用Kotlin语言,这为大家准备了《Kotlin开发学习指南》的核心笔记:
https://qr18.cn/CdjtAF
Android Kotlin 学习指南
概述
- Kotlin 用于服务器端
- Kotlin 用于 Android
- Kotlin 用于 JavaScript
- 新特性
Kotlin 基础包括操作、编码习惯与习惯用法
- 基本语法
- 习惯用法
- 编码规范
基础部分:
https://qr18.cn/CdjtAF
- 基本类型
- 包
- 控制流
-
返回与跳转
类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- ……
函数与 Lambda 表达式:
https://qr18.cn/CdjtAF
- 函数
- Lambda 表达式
- 内联函数
- 协程
- 其他
- 核心库
- Java 互操作
- JavaScript
- 工具
- 常见问题