您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

如何正确的在 Android 上使用协程?

时间:2019-10-22 14:01:38  来源:  作者:

前言

你还记得是哪一年的 google IO 正式宣布 Kotlin 成为 Android 一级开发语言吗?是 Google IO 2017 。如今两年时间过去了,站在一名 Android 开发者的角度来看,Kotlin 的生态环境越来越好了,相关的开源项目和学习资料也日渐丰富,身边愿意去使用或者试用 Kotlin 的朋友也变多了。常年混迹掘金的我也能明显感觉到 Kotlin 标签下的文章慢慢变多了(其实仍然少的可怜)。今年的 Google IO 也放出了 Kotlin First 的口号,许多新的 API 和功能特性将优先提供 Kotlin 支持。所以,时至今日,实在找不到安卓开发者不学 Kotlin 的理由了。

今天想聊聊的是 Kotlin Coroutine。虽然在 Kotlin 发布之初就有了协程,但是直到 2018 年的 KotlinConf 大会上,JetBrain 发布了 Kotlin1.3RC,这才带来了稳定版的协程。即使稳定版的协程已经发布了一年之余,但是好像并没有足够多的用户,至少在我看来是这样。在我学习协程的各个阶段中,遇到问题都鲜有地方可以求助,抛到技术群基本就石沉大海了。基本只能靠一些英文文档来解决问题。

关于协程的文章我看过很多,总结一下,无非下面几类。

第一类是 Medium 上热门文章的翻译,其实我也翻译过:

https://juejin.im/post/5cea3ee0f265da1bca51b841

https://juejin.im/post/5cee800051882544171c5a2c

https://juejin.im/post/5cf513d3e51d4577407b1ceb

说实话,这三篇文章的确加深了我对协程的理解。

第二类就是官方文档的翻译了,我看过至少不下于五个翻译版本,还是觉得看 https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html 比较好,如果英文看着实在吃力,可以对照着 Kotlin 中文站的翻译来阅读。

在看完官方文档的很长一段时间,我几乎只知道 GlobalScope。的确,官方文档上基本从头到尾都是在用 GlobalScope 写示例代码。所以一部分开发者,也包括我自己,在写自己的代码时也就直接 GlobalScope 了。一次偶然的机会才发现其实这样的问题是很大的。在 Android 中,一般是不建议直接使用 GlobalScope 的。那么,在 Android 中应该如何正确使用协程呢?再细分一点,如何直接在 Activity 中使用呢?如何配合 ViewModel 、LiveData 、LifeCycle 等使用呢?我会通过简单的示例代码来阐述 Android 上的协程使用,你也可以跟着动手敲一敲。

协程在 Android 上的使用

GlobalScope

在一般的应用场景下,我们都希望可以异步进行耗时任务,比如网络请求,数据处理等等。当我们离开当前页面的时候,也希望可以取消正在进行的异步任务。这两点,也正是使用协程中所需要注意的。既然不建议直接使用 GlobalScope,我们就先试验一下使用它会是什么效果。

private fun launchFromGlobalScope() {
 GlobalScope.launch(Dispatchers.Main) {
 val deferred = async(Dispatchers.IO) {
 // network request
 delay(3000)
 "Get it"
 }
 globalScope.text = deferred.await()
 Toast.makeText(ApplicationContext, "GlobalScope", Toast.LENGTH_SHORT).show()
 }
}

在 launchFromGlobalScope() 方法中,我直接通过 GlobalScope.launch() 启动一个协程,delay(3000) 模拟网络请求,三秒后,会弹出一个 Toast 提示。使用上是没有任何问题的,可以正常的弹出 Toast 。但是当你执行这个方法之后,立即按返回键返回上一页面,仍然会弹出 Toast 。如果是实际开发中通过网络请求更新页面的话,当用户已经不在这个页面了,就根本没有必要再去请求了,只会浪费资源。GlobalScope 显然并不符合这一特性。https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html 中其实也详细说明了,如下所示:

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Another use of the global scope is operators running in Dispatchers.Unconfined, which don’t have any job associated with them.

Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.

大致意思是,Global scope 通常用于启动顶级协程,这些协程在整个应用程序生命周期内运行,不会被过早地被取消。程序代码通常应该使用自定义的协程作用域。直接使用 GlobalScope 的 async 或者 launch 方法是强烈不建议的。

GlobalScope 创建的协程没有父协程,GlobalScope 通常也不与任何生命周期组件绑定。除非手动管理,否则很难满足我们实际开发中的需求。所以,GlobalScope 能不用就尽量不用。

MainScope

官方文档中提到要使用自定义的协程作用域,当然,Kotlin 已经给我们提供了合适的协程作用域 MainScope 。看一下 MainScope 的定义:

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

记着这个定义,在后面 ViewModel 的协程使用中也会借鉴这种写法。

给我们的 Activity 实现自己的协程作用域:

class BasicCorotineActivity : AppCompatActivity(), CoroutineScope by MainScope() {}

通过扩展函数 launch() 可以直接在主线程中启动协程,示例代码如下:

private fun launchFromMainScope() {
 launch {
 val deferred = async(Dispatchers.IO) {
 // network request
 delay(3000)
 "Get it"
 }
 mainScope.text = deferred.await()
 Toast.makeText(applicationContext, "MainScope", Toast.LENGTH_SHORT).show()
 }
}

最后别忘了在 onDestroy() 中取消协程,通过扩展函数 cancel() 来实现:

override fun onDestroy() {
 super.onDestroy()
 cancel()
}

现在来测试一下 launchFromMainScope() 方法吧!你会发现这完全符合你的需求。实际开发中可以把 MainScope 整合到 BaseActivity 中,就不需要重复书写模板代码了。

ViewModelScope

如果你使用了 MVVM 架构,根本就不会在 Activity 上书写任何逻辑代码,更别说启动协程了。这个时候大部分工作就要交给 ViewModel 了。那么如何在 ViewModel 中定义协程作用域呢?还记得上面 MainScope() 的定义吗?没错,搬过来直接使用就可以了。

class ViewModelOne : ViewModel() {
 private val viewModelJob = SupervisorJob()
 private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
 val mMessage: MutableLiveData<String> = MutableLiveData()
 fun getMessage(message: String) {
 uiScope.launch {
 val deferred = async(Dispatchers.IO) {
 delay(2000)
 "post $message"
 }
 mMessage.value = deferred.await()
 }
 }
 override fun onCleared() {
 super.onCleared()
 viewModelJob.cancel()
 }
}

这里的 uiScope 其实就等同于 MainScope。调用 getMessage() 方法和之前的 launchFromMainScope() 效果也是一样的,记得在 ViewModel 的 onCleared() 回调里取消协程。

你可以定义一个 BaseViewModel 来处理这些逻辑,避免重复书写模板代码。然而 Kotlin 就是要让你做同样的事,写更少的代码,于是 viewmodel-ktx 来了。看到 ktx ,你就应该明白它是来简化你的代码的。引入如下依赖:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03"

然后,什么都不需要做,直接使用协程作用域 viewModelScope 就可以了。viewModelScope 是 ViewModel 的一个扩展属性,定义如下:

val ViewModel.viewModelScope: CoroutineScope
 get() {
 val scope: CoroutineScope? = this.getTag(JOB_KEY)
 if (scope != null) {
 return scope
 }
 return setTagIfAbsent(JOB_KEY,
 CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
 }

看下代码你就应该明白了,还是熟悉的那一套。当 ViewModel.onCleared() 被调用的时候,viewModelScope 会自动取消作用域内的所有协程。使用示例如下:

fun getMessageByViewModel() {
 viewModelScope.launch {
 val deferred = async(Dispatchers.IO) { getMessage("ViewModel Ktx") }
 mMessage.value = deferred.await()
 }
}

写到这里,viewModelScope 是能满足需求的最简写法了。实际上,写完全篇,viewModelScope 仍然是我认为的最好的选择。

LiveData

Kotlin 同样为 LiveData 赋予了直接使用协程的能力。添加如下依赖:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03"

直接在 liveData {} 代码块中调用需要异步执行的挂起函数,并调用 emit() 函数发送处理结果。示例代码如下所示:

val mResult: LiveData<String> = liveData {
 val string = getMessage("LiveData Ktx")
 emit(string)
}

你可能会好奇这里好像并没有任何的显示调用,那么,liveData 代码块是在什么执行的呢?当 LiveData 进入 active 状态时,liveData{ } 会自动执行。当 LiveData 进入 inactive 状态时,经过一个可配置的 timeout 之后会自动取消。如果它在完成之前就取消了,当 LiveData 再次 active 的时候会重新运行。如果上一次运行成功结束了,就不会再重新运行。也就是说只有自动取消的 liveData{ } 可以重新运行。其他原因(比如 CancelationException)导致的取消也不会重新运行。

所以 livedata-ktx 的使用是有一定限制的。对于需要用户主动刷新的场景,就无法满足了。在一次完整的生命周期内,一旦成功执行完成一次,就没有办法再触发了。 这句话不知道对不对,我个人是这么理解的。因此,还是 viewmodel-ktx 的适用性更广,可控性也更好。

LifecycleScope

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03"

lifecycle-runtime-ktx 给每个 LifeCycle 对象通过扩展属性定义了协程作用域 lifecycleScope 。你可以通过 lifecycle.coroutineScope 或者 lifecycleOwner.lifecycleScope 进行访问。示例代码如下:

fun getMessageByLifeCycle(lifecycleOwner: LifecycleOwner) {
 lifecycleOwner.lifecycleScope.launch {
 val deferred = async(Dispatchers.IO) { getMessage("LifeCycle Ktx") }
 mMessage.value = deferred.await()
 }
}

当 LifeCycle 回调 onDestroy() 时,协程作用域 lifecycleScope 会自动取消。在 Activity/Fragment 等生命周期组件中我们可以很方便的使用,但是在 MVVM 中又不会过多的在 View 层进行逻辑处理,viewModelScope 基本就可以满足 ViewModel 中的需求了,lifecycleScope 也显得有点那么食之无味。但是他有一个特殊的用法:

suspend fun <T> Lifecycle.whenCreated()
suspend fun <T> Lifecycle.whenStarted()
suspend fun <T> Lifecycle.whenResumed()
suspend fun <T> LifecycleOwner.whenCreated()
suspend fun <T> LifecycleOwner.whenStarted()
suspend fun <T> LifecycleOwner.whenResumed()

可以指定至少在特定的生命周期之后再执行挂起函数,可以进一步减轻 View 层的负担。

总结

以上简单的介绍了在 Android 中合理使用协程的一些方案,示例代码已上传至 https://github.com/lulululbj/CoroutineDemo。关于 MVVM + 协程 的实战项目,可以看看我的开源项目 https://github.com/lulululbj/wanandroid,同时也期待你宝贵的意见。



Tags: Android   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
6 月 29 日,微软向 Windows 预览体验计划的 Dev 通道推送了 Windows 11 的第一个预览版本,我们也在第一时间升级到了最新系统,可以点击这里查看 APPSO 的抢先体验。 关于 Windo...【详细内容】
2021-07-14  Tags: Android  点击:(90)  评论:(0)  加入收藏
由于工作需要,需要解决一些性能问题,虽然有 Profiler 、Systrace 等工具, 但是无法实时监控,于是计划写一个能实时监控性能的小工具,经过学习大佬们的文章, 最终完成了这个开源的...【详细内容】
2021-01-14  Tags: Android  点击:(127)  评论:(0)  加入收藏
内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(o...【详细内容】
2020-12-22  Tags: Android  点击:(218)  评论:(0)  加入收藏
使用 Termux 和 Flask 在你的移动设备上创建、开发和运行一个网页应用。&bull; 来源:linux.cn &bull; 作者:Phani Adabala &bull; 译者:geekpi &bull;(本文字数:2923,阅读时长大...【详细内容】
2020-09-04  Tags: Android  点击:(42)  评论:(0)  加入收藏
近日,索尼发布了一款全新的车载中控屏幕设备&mdash;&mdash;XAV-AX5500,官方表示XAV-AX5500提供了强大的声音性能、流畅灵敏的触控屏,并且可以与用户的智能设备无缝集成。规格上...【详细内容】
2020-05-24  Tags: Android  点击:(65)  评论:(0)  加入收藏
你长得辣么好看,我想着要更详细地了解你。今天,让我们一起来聊聊 Android 的 GUI 系统。缘起在2019年的 Google I/O 大会上,Jetpack 团队首次为大家介绍了 Jetpack Compose,这是...【详细内容】
2020-01-06  Tags: Android  点击:(106)  评论:(0)  加入收藏
你的代码质量应该随着经验的增加而提高,在本文作者基于 Android 开发者六年间,其都学到了什么?作者 | Mrudula译者 | 弯月,责编 | 屠敏以下为译文:六年来,我为多家公司编写过各种...【详细内容】
2019-12-20  Tags: Android  点击:(71)  评论:(0)  加入收藏
注意:在windows环境下开发flutter应用建议采用win10,因为有win7下缺少开发依赖的组件1、到flutter官网下载安装文件,https://flutter.dev/docs/get-started/install2、下载完成...【详细内容】
2019-11-21  Tags: Android  点击:(106)  评论:(0)  加入收藏
本篇文章主要介绍 Android 9.0 Crash 机制部分知识点,通过阅读本篇文章,您将收获以下内容:一、Crash 概述 二、Crash处理流程 三、handleApplicationCrash处理分析 四、handle...【详细内容】
2019-11-13  Tags: Android  点击:(102)  评论:(0)  加入收藏
前言你还记得是哪一年的 Google IO 正式宣布 Kotlin 成为 Android 一级开发语言吗?是 Google IO 2017 。如今两年时间过去了,站在一名 Android 开发者的角度来看,Kotlin 的生态...【详细内容】
2019-10-22  Tags: Android  点击:(149)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(8)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(8)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(17)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(22)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(28)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(37)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(58)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(52)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条