本文是Android协程系列文章的第一部分。主要介绍协程是如何工作的,主要解决什么问题。协程用来解决什么问题?Kotlin中的协程提供了一种处理并发的新方法,您可以在Android平台上使用它来简化异步执行的代码。自Kotlin1.3版本以来就引入了协程,但这个概念从编程世界的黎明就已经存在了。最早使用协程的编程语言可以追溯到1967年的Simula语言。在过去的几年里,协程的概念发展迅速,并被许多主流编程语言所采用,如Javascript、C#、Python、Ruby、去。Kotlin的协程基于其他语言的既定概念。协程:https://kotlinlang.org/docs/reference/coroutines-overview.html模拟:https://en.wikipedia.org/wiki/SimulaJavascript:https://javascript.info/async-awaitC#:https//docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/Python:https://docs.python.org/3/library/asyncio-task.html红宝石:https://ruby-doc.org/core-2.1.1/Fiber.htmlGo:https://tour.golang.org/concurrency/1在Android平台上,协程主要用来解决两个问题:处理消耗长时间运行任务,这类任务经常阻塞主线程;确保主线程安全(Main-safety),即确保任何挂起函数都是从主线程安全调用的。让我们深入研究上述问题,看看如何在我们的代码中实现协程。处理耗时的任务获取网络内容或与远程API交互涉及发送网络请求,从数据库获取数据或从磁盘读取图像资源涉及读取文件。通常我们将此类操作归类为耗时任务——应用程序会停止并等待它们被处理,这会消耗大量时间。今天的手机处理代码的速度比处理网络请求的速度快得多。以Pixel2为例,单个CPU周期耗时不到0.0000000004秒。这个数字很难用人类语言来表达。但是,如果把网络请求用“一眨眼”来表示,大约是400毫秒(0.4秒),那么就更容易理解CPU有多快了。仅仅一眨眼的功夫,或者说处理一个慢速网络请求所花费的时间,CPU就已经完成了超过10亿个时钟周期。Android中的每个应用程序都会运行一个主线程,主要用于处理UI(如绘制界面)和协调用户交互。如果主线程需要处理的任务太多,应用就会运行缓慢,看起来就像是“卡住了”一样,会极大地影响用户体验。所以如果想让应用运行起来不“卡顿”,让动画能够流畅运行或者能够快速响应用户的点击事件,就要防止那些耗时的任务阻塞主线程的运行。要在不阻塞主线程的情况下处理网络请求,一种常见的方法是使用回调。回调是稍后执行你的回调代码。这样,请求developer.android.google.cn网站数据的代码就会类似下面这样:classViewModel:ViewModel(){funfetchDocs(){get("developer.android.google.cn"){result->show(result)}}}在上面的例子中,即使在主线程中调用了get,它也会使用另一个线程来执行网络请求。一旦网络请求返回结果并且结果可用,回调代码将被主线程调用。这是处理耗时任务的好方法,像Retrofit这样的库通过处理网络请求而不阻塞主线程执行来为您做到这一点。Retrofi:https://square.github.io/retrofit/使用协程来处理协程任务使用协程可以简化你的代码来处理像fetchDocs这样耗时的任务。我们先用协程的方式重写上面的代码,说明协程是如何处理耗时任务的,让代码更清晰简洁。//Dispatchers.MainsuspendfunfetchDocs(){//Dispatchers.Mainvalresult=get("developer.android.google.cn")//Dispatchers.Mainshow(result)}//下一章查看这段代码suspendfunget(url:String)=withContext(Dispatchers.IO){/*...*/}在上面的例子中,你可能会有很多疑惑,它不会阻塞主线程吗?get方法如何不等待网络Request和线程阻塞返回结果?事实上,Kotlin中的协程提供了这种在不阻塞主线程的情况下执行代码的方法。协程向常规函数添加了两个新操作。协程除了invoke(或call)和return之外,还增加了suspend和resume:suspend——也称为suspend或pause,用于暂停当前协程的执行并保存所有局部变量;resume——用来让挂起的协程程序从挂起的地方恢复执行。Kotlin通过添加suspend关键字来实现上述功能。只能在一个suspend函数中调用另一个suspend函数,或者通过协程构造器(如launch)启动一个新的协程。launch:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html(1)使用suspend和resume代替callback上例中,getstillRunsonthemain线程,但它会在启动网络请求之前暂停协程。当网络请求完成时,get恢复挂起的协程,而不是使用回调通知主线程。上面的动画展示了Kotlin如何使用挂起和恢复而不是回调。观察上图中fetchDocs的执行,了解suspend是如何工作的。Kotlin使用堆栈帧来管理要运行的函数和所有局部变量。当协程挂起时,当前堆栈帧被复制并保存以备后用。当协程恢复时,堆栈帧从它保存的地方复制回来,函数再次开始运行。在上面的动画中,当主线程下的所有协程都挂起后,主线程在处理屏幕绘制和点击事件时就没有压力了。所以用上面的suspend和resume操作代替回调看起来很爽。(2)当主线程下的所有协程都挂起后,主线程处理其他事件不会有压力即使代码可能看起来像普通的顺序阻塞请求,协程也可以保证网络请求不会阻塞主线程线。接下来,让我们看看协程如何确保主安全并讨论调度器。使用协程的主线程安全在Kotlin的协程中,主线程调用编写良好的挂起函数通常是安全的。不管这些挂起函数做什么,它们都应该允许任何线程调用它们。但是在我们的Android应用中有很多事情处理起来太慢,不应该在主线程上完成,比如网络请求,解析JSON数据,数据库读写,甚至遍历比较大的数组。这些导致执行时间长,让用户有“卡顿”感的操作,不应该在主线程上执行。使用suspend并不意味着告诉Kotlin在后台线程上执行函数,这里的重点是协程将在主线程上运行。实际上,在响应UI事件启动协程时,使用Dispatchers.Main.immediate是一个非常好的选择,这样即使需要保证主线程安全的耗时任务不在协程中执行end,可以在下一个执行框架中提供给用户可用的执行结果。Dispatchers.Main.immediateh:ttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-coroutine-dispatcher/immediate.html(1)协程会在mainthread正在运行,挂起不代表后台执行。如果您需要处理一个太耗时而无法在主线程上执行的函数,但又想在主线程上保持该函数的安全,您可以让Kotlin协程在Default或IO调度器上执行工作。在Kotlin中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。协程可以自行挂起,调度程序负责恢复它们。Kotlin提供了三个调度程序,您可以使用它们来指定协程应该在何处运行:如果您在Room中使用挂起函数、RxJava或LiveData,Room会自动使主线程安全。Retrofit和Volley等网络库管理它们自己使用的线程,因此当您在Kotlin协程中调用这些库中的代码时,无需处理主线程安全问题。调度程序:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/暂停:https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5RxJava:https://medium.com/androiddevelopers/room-rxjava-acb0cd4f3757实时数据:https://developer.android.google.cn/topic/libraries/architecture/livedata#use_livedata_with_room房间:https://developer.android.google。cn/topic/libraries/architecture/roomRetrofit:https://square.github.io/retrofit/Volley:https://developer.android.google.cn/training/volley按照前面的例子,可以使用scheduler重新定义get函数。在get的主体内,调用withContext(Dispatchers.IO)创建一个在IO线程池中运行的块。您放入该块中的任何代码始终通过IO调度程序执行。由于withContext本身是一个挂起函数,所以它使用协程来保证主线程安全。//Dispatchers.MainsuspendfunfetchDocs(){//Dispatchers.Mainvalresult=get("developer.android.google.cn")//Dispatchers.Mainshow(result)}//Dispatchers.Mainsuspendfunget(url:String)=//Dispatchers.MainwithContext(Dispatchers.IO){//Dispatchers.IO}//Dispatchers.Main使用协程,您可以调度具有细粒度控制的线程。由于withContext允许您在不引入回调的情况下控制任何代码行的线程池,因此您可以将其应用于非常小的功能,例如从数据库读取数据或执行网络请求。一个好的做法是使用withContext使每个函数都是主线程安全的,这意味着您可以从主线程调用每个函数。这样,调用者就不再需要考虑应该使用哪个线程来执行函数。在此示例中,fetchDocs将在主线程上执行,但是,它可以安全地调用get在后台执行网络请求。因为协程支持暂停和恢复,一旦withContext块完成,主线程上的协程将恢复执行。(2)主线程调用编写良好的挂起函数通常是安全的。确保每个挂起函数都是主线程安全的很有用。如果一个任务需要接触磁盘、网络,甚至只是占用太多CPU,你应该使用withContext来确保它可以从主线程安全地调用。这也是Retrofit和Room等代码库背后的原则。如果你在编写代码的过程中遵循这一点,你的代码将会非常简单,并且不会将线程问题与应用程序逻辑混为一谈。同时,在这个原则下,协程也可以被主线程自由调用,网络请求或者数据库操作的代码变得非常简洁,也可以保证用户在这个过程中不会有“卡顿”的感觉使用应用程序。withContext的性能可与回调或RxJava相媲美,后者提供主线程安全功能。在某些情况下,甚至可以优化withContext调用以优于等效的基于回调的实现。如果一个函数需要10次调用数据库,您可以使用外部withContext告诉Kotlin只切换一次线程。这样,即使数据库的代码库不断调用withContext,它也会保持在同一个调度程序上并遵循快速路径,从而保持性能。此外,Dispatchers.Default和Dispatchers.IO之间的切换已经过优化,以尽可能避免线程切换的性能损失。下一步本文介绍了使用协程可以解决什么样的问题。协程在计算机编程语言领域是一个古老的概念,但是因为可以让网络请求的代码更加简洁,所以又重新流行起来。在Android平台上,可以使用协程来处理两个常见的问题:简化处理网络请求、磁盘读取甚至大型JSON数据解析等耗时任务;在降低代码复杂度和保证代码可读性的前提下,不会阻塞主线程的执行。下一篇:《在 Android 开发中使用协程 | 上手指南》【本文为栏目组织“GoogleDevelopers”原创稿件,转载请联系原作者(微信公众号:Google_Developers)】点此查看作者更多好文
