Android 上的协程(第一部分):了解背景
Posted Calvin880828
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 上的协程(第一部分):了解背景相关的知识,希望对你有一定的参考价值。
android 上的协程(第一部分):了解背景
这篇文章重点介绍协程的工作原理以及它们解决的问题。
协程解决什么问题?
Kotlin协程引入了一种新的并发方式,可以在 Android 上使用它来简化异步代码。虽然协程在 1.3 中是 Kotlin 的新概念,但协程的概念自编程语言诞生以来就一直存在。第一个探索使用协程的语言是1967 年的Simula。
在过去的几年中,协程越来越受欢迎,现在已包含在许多流行的编程语言中,例如javascript、C#、Python、Ruby和Go等等。Kotlin 协程基于已用于构建大型应用程序的既定概念。
在 Android 上,协程可以很好地解决两个问题:
长时间运行的任务是花费太长时间阻塞主线程的任务。
Main-safety允许您确保可以从主线程调用任何挂起函数。
让我们深入了解协程,看看协程如何帮助我们以更简洁的方式构建代码!
长时间运行的任务
获取网页或与 API 交互都涉及发出网络请求。类似地,从数据库读取或从磁盘加载图像涉及读取文件。这些类型的事情就是我所说的长时间运行的任务——这些任务花费的时间太长以至于您的应用程序无法停止并等待它们!
与网络请求相比,现代手机执行代码的速度可能很难理解。在 Pixel 2 上,单个 CPU 周期仅需不到 0.0000000004 秒,这个数字很难用人类的术语来理解。但是,如果您将网络请求视为一眨眼,大约 400 毫秒(0.4 秒),就更容易理解 CPU 的运行速度。一眨眼的功夫,或者一个有点慢的网络请求,CPU 可以执行超过 10 亿个周期!
在 Android 上,每个应用程序都有一个主线程,负责处理 UI(如绘图视图)和协调用户交互。如果此线程上发生过多工作,应用程序似乎会挂起或变慢,从而导致不良的用户体验。任何长时间运行的任务都应该在不阻塞主线程的情况下完成,这样您的应用程序就不会显示所谓的“卡顿”,如冻结的动画,或者对触摸事件的响应缓慢。
为了在主线程之外执行网络请求,一个常见的模式是回调。回调提供了一个库的句柄,它可以用来在将来某个时间回调到您的代码中。使用回调的话,获取developer.android.com
上的数据可能是下面的样子:
class ViewModel: ViewModel()
fun fetchDocs()
get("developer.android.com") result ->
show(result)
即使get
是从主线程调用,它也会使用另一个线程来执行网络请求。然后,一旦网络上的结果可用,就会在主线程上调用回调。这是处理长时间运行任务的好方法,像Retrofit
这样的库可以帮助您在不阻塞主线程的情况下发出网络请求。
使用协同程序处理长时间运行的任务
协程是一种简化用于管理长时间运行任务(如fetchDocs. 为了探索协程如何使长时间运行的任务的代码更简单,让我们重写上面的回调示例以使用协程。
// Dispatchers.Main
suspend fun fetchDocs()
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO)/*...*/
这段代码不会阻塞主线程吗?get
它如何从没有返回结果等待网络请求并阻塞?它事实证明,协程为 Kotlin 提供了一种执行此代码且从不阻塞主线程的方法。
协程通过添加两个新操作建立在常规函数之上。除了invoke(或 call)和return之外,协程还添加了suspend
和resume
。
- suspend — 暂停当前协程的执行,保存所有局部变量
- resume — 从暂停的地方继续暂停的协程
此功能由 Kotlin 通过函数上的 suspend 关键字添加。您只能从其他挂起函数调用挂起函数,或者使用协程构建器(如launch启动新协程)。
Suspend 和 Resume 一起工作来代替回调。
在上面的示例中,get将在启动网络请求之前暂停协程。该函数get仍将负责在主线程之外运行网络请求。然后,当网络请求完成时,它可以简单地恢复它挂起的协程,而不是调用回调来通知主线程。
查看如何fetchDocs执行,您可以了解suspend
的工作原理。每当协程挂起时,都会复制并保存当前堆栈帧(Kotlin 用来跟踪正在运行的函数及其变量的位置)。当它恢复时,堆栈帧从它保存的地方被复制回来并再次开始运行。在动画中间——当主线程上的所有协程都挂起时,主线程可以自由地更新屏幕和处理用户事件。suspend 和 resume 一起替换回调。很简约!
当主线程上的所有协程都挂起后,主线程就可以自由地做其他工作了。
即使我们编写了看起来完全像阻塞网络请求的简单顺序代码,协程也会按照我们的需要运行我们的代码并避免阻塞主线程!
接下来,让我们看看如何使用协程来实现主安全并探索调度程序。
协同程序的主要安全性
在 Kotlin 协程中,编写良好的挂起函数始终可以安全地从主线程调用。不管他们做什么,他们应该总是允许任何线程调用他们。
但是,我们在 Android 应用程序中做的很多事情都太慢而无法在主线程上发生。网络请求、解析 JSON、从数据库读取或写入,甚至只是遍历大型列表。其中任何一个都有可能运行缓慢到足以导致用户可见的“卡顿”,并且应该在主线程之外运行。
使用suspend
不会告诉 Kotlin 在后台线程上运行函数。值得清楚且经常说的是协程将在主线程上运行。事实上,在启动协程以响应 UI 事件时使用Dispatchers.Main.immediate
是一个非常好的主意——这样,如果您最终没有执行需要 main-safety 的长时间运行的任务,结果可以在下一帧中为用户提供。
协程会运行在主线程上,挂起不代表后台。
Default要使一个对于主线程来说工作速度太慢的函数是主安全的,您可以告诉 Kotlin 协程在 the或dispatcher上执行工作IO。在 Kotlin 中,所有协程都必须在调度程序中运行——即使它们在主线程上运行也是如此。协程可以自行暂停,而调度程序知道如何恢复它们。
为了指定协程应该在何处运行,Kotlin 提供了三个可用于线程分派的分派器。
+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+
如果您使用挂起函数、RxJava或LiveData , Room将自动提供主安全。
Retrofit和Volley等网络库管理它们自己的线程,并且在与 Kotlin 协程一起使用时不需要在代码中显式维护主安全。
继续上面的示例,让我们使用调度程序来定义函数get。get在您调用的主体内withContext(Dispatchers.IO)
创建一个将在IO调度程序上运行的块。您放入该块内的任何代码将始终在调度程序上执行IO。由于withContext
它本身是一个挂起函数,它将使用协程来提供主要安全性。
// Dispatchers.Main
suspend fun fetchDocs()
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.Main
withContext(Dispatchers.IO)
// Dispatchers.IO
/* perform blocking network IO here */
// Dispatchers.Main
使用协程,您可以使用细粒度控制进行线程分派。因为withContext
允许您控制任何代码行在哪个线程上执行而无需引入回调来返回结果,所以您可以将它应用于非常小的功能,例如从数据库读取或执行网络请求。因此,一个好的做法是withContext
确保每个函数都可以安全地在任何Dispatcher包含上调用Main——这样调用者就不必考虑执行该函数需要什么线程。
在这个例子中,fetchDocs
在主线程上执行,但可以安全地调用get它在后台执行网络请求。因为协程支持suspend
和resume
,一旦块withContext
完成,主线程上的协程将恢复结果。
编写良好的挂起函数总是可以安全地从主线程(或主线程)调用。
使每个挂起函数都是主线程安全的是一个非常好的主意。如果它做任何涉及磁盘、网络的事情,甚至只是使用过多的 CPU,请使用withContext它来确保从主线程调用是安全的。这是 Retrofit 和 Room 等基于协程的库所遵循的模式。如果您在整个代码库中都遵循这种风格,您的代码将会更加简单,并且可以避免将线程问题与应用程序逻辑混在一起。如果始终遵循,协程可以在主线程上自由启动并使用简单的代码发出网络或数据库请求,同时保证用户不会看到“卡顿”。
withContext的表现
withContext
与提供主要安全性的回调或 RxJava 一样快。在某些情况下,甚至可以优化withContext
调用,使其超出回调所能达到的范围。如果一个函数将对数据库进行 10 次调用,您可以告诉 Kotlin 在所有 10 次调用周围切换一次withContext
。然后,即使数据库 library 会对withContext
重复调用,它也会留在同一个调度程序上并遵循快速路径。此外,对Dispatchers.Default
和之间的切换Dispatchers.IO
进行了优化,以尽可能避免线程切换。
在这篇文章中,我们探讨了协程最擅长解决的问题。协程是编程语言中一个非常古老的概念,最近变得流行,因为它们能够简化与网络交互的代码。
在 Android 上,您可以使用它们来解决两个非常常见的问题:
1.简化长时间运行任务的代码,例如从网络、磁盘读取,甚至解析大型 JSON 结果。
2.执行精确的 main-safety 以确保您不会意外阻塞主线程而不会使代码难以读写。
超长文,带你全面了解Kotlin的协程
https://me.csdn.net/NJP_NJP
1. 什么是异步
我记得小学二年级碰到过一个让我受益终身的数学题: 烧开水需要15分钟,洗碗需要5分钟,扫地需要5分钟,请问做完这三件事,总共需要几分钟?从此我做什么事,都事先想想先后顺序,看看可不可以一并去做。
长大后才知道这就是异步的用法,它其实已经渗透到你的生活中。
2. 为什么需要回调
3. 回调的缺点
//烧2000mL热水来洗碗
boilWater(2000) { water ->
washDishes(water)
}
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
request2(value1) { value2 ->
request3(value2) { value3 ->
updateUI(value3)
}
}
}
request1(parameter)
.map { value1 ->
request2(value1)
}.map { value2 ->
request3(value2)
}.subscribe { value3 ->
updateUI(value3)
}
//1.简单秩序的串行世界:
print("Hello ")
print("World!")
//结果为:Hello World!
//2.复杂混沌的并行世界:
Thread {
Thread.sleep(2000)
print("Hello ")
}.start()
print("World!")
val value1 = request1(parameter)
val value2 = request2(value1)
val value3 = request2(value2)
updateUI(value3)
request1(parameter) { value1 ->
request2(value1) { value2 ->
request3(value2) { value3 ->
updateUI(value3)
}
}
}
1. 添加依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
2. 启动协程
GlobalScope.launch {
delay(1000L)
println("Hello,World!")
}
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit): Job {...}
CoroutineScope翻译过来就是“协程范围”,指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行,上面第一段代码中的GlobalScope指的是与应用进程相同的协程范围,也就是在进程没有结束之前协程内的代码都可以运行。
-
context:协程上下文,可以指定协程运行的线程。默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。
-
start:协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。
-
block:协程主体。也就是要在协程内部运行的代码,可以通过lamda表达式的方式方便的编写协程内运行的代码。
-
CoroutineExceptionHandler:除此之外还可以指定CoroutineExceptionHandler来处理协程内部的异常。
3. 调用挂起函数
println("Start")
GlobalScope.launch(Dispatchers.Main) {
delay(1000L)
println("Hello World")
}
println("End")
println("Start")
Thread {
Thread.sleep(1000L)
println("Hello World")
}.start()
println("End")
//协程代码
println("Start ${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) {
delay(1000L)
println("Hello World ${Thread.currentThread().name}")
}
println("End ${Thread.currentThread().name}")
//线程代码
println("Start ${Thread.currentThread().name}")
Thread {
Thread.sleep(1000L)
println("Hello World ${Thread.currentThread().name}")
}.start()
println("End ${Thread.currentThread().name}")
public suspend fun delay(timeMillis: Long) {...}
GlobalScope.launch(Dispatchers.Main) {
println("Hello ${Thread.currentThread().name}")
test()
println("End ${Thread.currentThread().name}")
}
suspend fun test(){
println("World ${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Main) {
println("Hello ${Thread.currentThread().name}")
test()
println("End ${Thread.currentThread().name}")
}
suspend fun test(){
withContext(Dispatchers.IO){
println("World ${Thread.currentThread().name}")
}
}
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
request2(value1) { value2 ->
request3(value2) { value3 ->
updateUI(value3)
}
}
}
//用协程改造回调代码
GlobalScope.launch(Dispatchers.Main) {
//三次请求顺序执行
val value1 = request1(parameter)
val value2 = request2(value1)
val value3 = request2(value2)
//用最终结果更新UI
updateUI(value3)
}
//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}
//并发请求
GlobalScope.launch(Dispatchers.Main) {
//三次请求并发进行
val value1 = async { request1(parameter1) }
val value2 = async { request2(parameter2) }
val value3 = async { request3(parameter3) }
//所有结果全部返回后更新UI
updateUI(value1.await(), value2.await(), value3.await())
}
//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}
//复杂业务逻辑的Kotlin协程实现
GlobalScope.launch(Dispatchers.Main) {
//首先拿到request1的请求结果
val value1 = request1(parameter1)
//将request1的请求结果用于request2和request3两个请求的并发进行
val value2 = async { request2(value1) }
val value3 = async { request2(value1) }
//用request2和request3两个请求结果更新UI
updateUI(value2.await(), value3.await())
}
//requestAPI适配了Kotlin协程
suspend fun request1(parameter : Parameter){...}
suspend fun request2(parameter : Parameter){...}
suspend fun request3(parameter : Parameter){...}
1. 添加依赖
//添加Retrofit网络库和gsonConverter的依赖,注意一定要2.6.0版本以上
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
//添加Jetpack中架构组件的依赖,注意viewmodel要添加viewmodel-ktx的依赖
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
//添加Glide的依赖用于图片加载
implementation 'com.github.bumptech.glide:glide:4.10.0'
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="10dp"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:text="refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
<ImageView
android:id="@+id/imageView2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
<ImageView
android:id="@+id/imageView3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
</LinearLayout>
3. 编写网络层接口
{
"code": "200",
"imgurl": "https://img02.sogoucdn.com/app/a/100520113/20140811192414"
}
data class ImageDataResponseBody(
val code: String,
val imgurl: String
)
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import retrofit2.http.GET
import retrofit2.http.Query
//网络接口
interface ApiService {
//声明为suspend方法
@GET("image/sogou/api.php")
suspend fun getImage(@Query("type") type: String = "json"): ImageDataResponseBody
}
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.util.concurrent.TimeUnit
//网络层访问统一入口
object NetworkService {
//retorfit实例,在这里做一些统一网络配置,如添加转换器、设置超时时间等
private val retrofit = Retrofit.Builder()
.client(OkHttpClient.Builder().callTimeout(5, TimeUnit.SECONDS).build())
.baseUrl("https://api.ooopn.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
//网络层访问服务
val apiService = retrofit.create<ApiService>()
}
sealed class LoadState(val msg: String) {
class Loading(msg: String = "") : LoadState(msg)
class Success(msg: String = "") : LoadState(msg)
class Fail(msg: String) : LoadState(msg)
}
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.xxx.coroutinesdemo.bean.LoadState
import com.xxx.coroutinesdemo.network.NetworkService
class MainViewModel : ViewModel() {
//存放三张图片的url数据
val imageData = MutableLiveData<List<String>>()
//存放网路加载状态信息
val loadState = MutableLiveData<LoadState>()
//从网络加载数据
fun getData() {...}
}
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.bumptech.glide.Glide
import com.xxx.coroutinesdemo.R
import com.xxx.coroutinesdemo.bean.LoadState
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//获取ViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
//对加载状态进行动态观察
viewModel.loadState.observe(this, Observer {
when (it) {
is LoadState.Success -> button.isEnabled = true
is LoadState.Fail -> {
button.isEnabled = true
Toast.makeText(this, it.msg, Toast.LENGTH_SHORT).show()
}
is LoadState.Loading -> {
button.isEnabled = false
}
}
})
//对图片Url数据进行观察
viewModel.imageData.observe(this, Observer {
//用Glide加载三张图片
Glide.with(this)
.load(it[0])
.into(imageView1)
Glide.with(this)
.load(it[1])
.into(imageView2)
Glide.with(this)
.load(it[2])
.into(imageView3)
})
//点击刷新按钮来网络加载
button.setOnClickListener {
viewModel.getData()
}
}
}
5. 实现getData方法
fun getData() {
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
//加载失败的状态
loadState.value = LoadState.Fail(e.message ?: "加载失败")
}) {
//更新加载状态
loadState.value = LoadState.Loading()
//并发请求三张图片的数据
val data1 = async { NetworkService.apiService.getImage() }
val data2 = async { NetworkService.apiService.getImage() }
val data3 = async { NetworkService.apiService.getImage() }
//通过为LiveData设置新的值来触发更新UI
imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
it.imgurl
}
//更新加载状态
loadState.value = LoadState.Success()
}
}
/**
* ...
* This scope is bound to [Dispatchers.Main]
*/
val ViewModel.viewModelScope: CoroutineScope
get() {...}
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
fun ViewModel.launch(
block: suspend CoroutineScope.() -> Unit,
onError: (e: Throwable) -> Unit = {},
onComplete: () -> Unit = {}
) {
viewModelScope.launch(CoroutineExceptionHandler { _, e -> onError(e) }) {
try {
block.invoke(this)
} finally {
onComplete()
}
}
}
-
block:协程主体; -
onError:错误回调; -
onComplete:完成回调。
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.xxx.coroutinesdemo.bean.LoadState
import com.xxx.coroutinesdemo.launch
import com.xxx.coroutinesdemo.network.NetworkService
import kotlinx.coroutines.async
class MainViewModel : ViewModel() {
val imageData = MutableLiveData<List<String>>()
val loadState = MutableLiveData<LoadState>()
fun getData() {
launch(
{
loadState.value = LoadState.Loading()
val data1 = async { NetworkService.apiService.getImage() }
val data2 = async { NetworkService.apiService.getImage() }
val data3 = async { NetworkService.apiService.getImage() }
imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
it.imgurl
}
loadState.value = LoadState.Success()
},
{
loadState.value = LoadState.Fail(it.message ?: "加载失败")
}
)
}
}
{
"code": 200,
"data": {...},
"msg": "OK"
}
data class ResponseBody<T>(
val code: Int,
val msg: String,
val data: T
)
object Repository {
//数据脱壳与错误预处理
fun <T> preprocessData(responseBody: ResponseBody<T>): T {
return if (responseBody.code == 200) responseBody.data else throw Throwable(responseBody.msg)
}
suspend fun getImageData(paramter: Paramter1): ImageData {
//调用ApiService定义的接口方法
val responseBody = ApiService.getImage(paramter)
//返回处理后的数据
return preprocessData<ImageData>(responseBody)
}
suspend fun getOtherData(paramter: Paramter2): OtherData {...}
...
}
https://github.com/NaJiPeng/Coroutines-Demo
以上是关于Android 上的协程(第一部分):了解背景的主要内容,如果未能解决你的问题,请参考以下文章
“无法访问主线程上的数据库,因为它可能会长时间锁定 UI。”我的协程错误
python基础===基于requests模块上的协程trip