前言
我们在学习一些新技术的时候,首先会关注的他的应用场景以及一些使用有点,满足我们的需求后在项目中使用,然后再研究底层的实现原理及本质,在遇到问题的时候能够快速解决。因此对协程,我们的首要目标还是熟练使用。
协程对于Java开发人员来说相对陌生,Java语言本身没协程概念。Kotlin从版本1.3中才引入进来的。官方解释协程一种并发设计模式,使用它来简化异步编程代码,用同步的编码方式实现异步的效果。下面通过一些示例来说明协程的使用。
协程使用
添加依赖项
// 添加kotlinx-coroutines-android依赖会自动引入kotlinx-coroutines-core-jvm
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
一、在Fragment或者Activity中使用协程
在业务的开发中,我们或多或少会在UI控制器中执行一些耗时或者阻塞主线程的操作,以前大多都是自己写线程或者使用AsyncTask来实现。
下面通过读取文件md5值来演示协程在Fragment或者Activity中的使用。
在APK的下载安装过程中,下载完成后一般我们都会进行MD5校验,保证文件的完整性,然后再进行安装,如果apk的比较小,即使在主线程进行MD5比对,也没有太大的影响,如果APK比较大,在主线程进行对比的话就会有明显的卡顿现象。下面结合协程来实现该需求。
1.1 使用lifecycle扩展库来创建协程
为了避免内存泄漏,我们需要考虑在UI控制的生命周期内使用协程,生命周期结束后结束协程。为此,依赖官网封装好的lifecycle扩展库:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha03'
下面是校验MD5代码:
private fun checkApkMD5(fileMD5: String) {
val file = File("")
lifecycleScope.launch {
val md5 = withContext(Dispatchers.IO) {
getFileMd5(file)
}
if (fileMD5 == md5){
//install apk
}
}
}
private fun getFileMd5(file: File): String? {
......
return null
}
上面的代码演示了利用协程来完成了文件的md5读取,下面分析一下checkApkMD5函数中的协程相关代码:
- 我们知道协程都必须在一个作用域内(CoroutineScope)运行,一个CoroutineScope管理一个或者多个相关的协程,因此我们需要创建一个CoroutineScope。这里我们使用lifecycleScope
- lifecycleScope是LifecycleOwner的扩展属性LifecycleCoroutineScope,也就是定义了一个协程作用域,内部会在LifeCycle生命周期结束后自动取消协程。
- launch 是一个函数,用户创建协程并将函数主体分派给相应的调度程序。
- lifecycleScope的默认调度程序Dispatchers.Main
- 通过withContext将文件MD5的读取操作移至I/O线程。
最后checkApkMD5函数按以下方式执行:
- 在主线程中运行checkApkMD5函数
- 通过lifecycleScope launch创建一个新的协程,然后主线中执行协程的函数主体
- 运行到withCotext()块的时候,会挂起协程
- 在withCotext()块中,将文件的MD5读取操作移至I/O线程
- withCotext块结束运行后,协程执行恢复操作,回到主线程继续执行,完成MD5的校验以及apk的安装
通过上面的代码以及执行过程分析,我们看到,文件的MD5读取放在I/O线程,读取完成后又恢复到主线程。对比Java语言的开发,这里明显的减少异步的回调代码,看起来就是同步的代码风格完成了异步的执行流程。因此在Android中使用协程的核心竞争力就是:简化异步并发代码方式,用同步的方式写出了异步代码。
1.2 自定义CoroutineScope来创建协程
lifecycleScope是Lifecycle的作用域,当然我们创建自己的CoroutineScope,如下:
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private fun checkApkMD5(fileMD5: String) {
val file = File("")
scope.launch {
val md5 = withContext(Dispatchers.IO) {
println("threadName:"+Thread.currentThread().name)
getFileMd5(file)
}
println("threadName:"+Thread.currentThread().name)
if (fileMD5 == md5) {
//install apk
}
}
}
//UI控制器生命周期结束的时候取消相关协程
override fun onDestroy() {
super.onDestroy()
clearUp()
}
private fun clearUp() {
scope.cancel()
}
自定义CoroutineScope和使用lifecycleScope的功能一样,只是自定义的CoroutineScope我们需要手动在生命结束时取消作用域。而lifecycleScope系统已经帮我们做好了封装。因此单从方面使用的角度来讲,还是建议使用lifecycleScope。
二、网络请求中使用协程
我们平时开发App,一般遵循谷歌推荐的应该用架构指南,ui+viewModel + repository + remoteData,其中remoteData利用retrofit实现。
目前大多数接口返回的都是json格式的数据,因此先:
2.1 定义一个接收对象
private const val CODE_SUCCESS = 0
class ApiResponse<T> {
var code: Int = -1
var message: String? = null
var data: T? = null
fun isSuccess(): Boolean {
return code == CODE_SUCCESS
}
}
2.2、定义Retrofit接口API
interface PersonApi {
@GET("/user/userinfo")
suspend fun loadUser(@Query("userId") userId: String): ApiResponse<UserInfo>
}
2.3、在repository中获取数据 获取数据主要做线程的切换操作,代码如下:
class UserRepository(private val personApi: PersonApi) {
suspend fun loadUserInfo(userId: String): ApiResponse<UserInfo> {
return withContext(Dispatchers.IO) {
personApi.loadUser(userId)
}
}
}
这里personApi.loadUser需要在I/O线程中进行操作,我们通过withContext()将其移至I/O操作线程,其中withContext是挂起函数,需要在协程或者挂起函数中执行,因此需要将loadUserInfo定义为挂起函数。
2.4、在viewModel中通过repository获取数据
获取数据主要通过以下代码实现:
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _user = MutableLiveData<Resource<UserInfo>>()
val user = _user
//获取用户信息
fun loadUser(userId: String) {
viewModelScope.launch {
try {
val apiResponse = userRepository.loadUserInfo(userId)
if (apiResponse.isSuccess()) {
user.value = Resource.Success(apiResponse.data)
} else {
user.value = Resource.Error(apiResponse.message)
}
} catch (ex: Exception) {
ex.printStackTrace()
user.value = Resource.Error(ex.message)
}
}
}
}
2.5、分析执行流程
下面我们分析loadUser函数的执行过程:
- 通过viewModelScope的launch创建一个新的协程,在主线程上发出网络请求,然后该协程开始执行。其中viewModelScope中的CoroutineContext会在viewModel的销毁时执行cancel()操作。
- 在协程内,执行userRepository.loadUserInfo()会挂起协程,直至loadUserInfo()中的withContext代码块运行结束
- withContext代码块运行结束后,loadUser()中的协程在主线程上恢复执行操作,并返回网络请求的结果
协程在网络请求中的使用大致流程如上面代码所示,但是到了具体的项目中需要根据实际需求进一步调整。
注意: 上面的网络请求示例只是为了展示协程的使用,正在的项目开发中还需要进一步的封装,减少不必要的样板代码。后续结合Flow一起来使用,更能发挥其价值。
三、在App进程生命周期内使用CoroutineScope
在我们App的开发中,也许会出现这种业务场景,用户退出当前UI后我们需要做一些操作,比如记录日志,向服务器发送数据。这种场景下使用viewModelScope或者lifecycleScope就不合适,因为这个两个做用户会在对应的UI生命周期结束后取消协程。下面通过一个具体的案例是说明这种业务场景中协程的使用。
3.1 需求
当前UI有一个点赞按钮,用户点击该按钮有点赞获取取消点赞两种功能,通常情况下用户点击一次我们提交一次数据。但是用户可以不停的点击,这种场景下服务端就会要求我们前端在用户离开UI后再提交点赞数据,从而减少对服务端的压力。
3.2 具体实现
3.2.1 定义CoroutineScope
为了实现上面的需求,我们定义一个Application级别的CoroutineScope:
class MyApplication : Application() {
companion object {
//整个APP的协程作用域,生命周期跟随该进程,不同于viewModelScope或者lifecycleScope
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
}
提示: 为了更方便我们定制CoroutineScope,建议我们手动定义CoroutineScope,而不使用GlobalScope
3.2.2 实现UI对应的ViewModel
class MyViewModel(
private val repository: NyRepository,
private val applicationScope: CoroutineScope
) : ViewModel() {
...省略其他业务代码...
......
fun reportData() {
applicationScope.launch {
repository.reportData().collect { }
}
}
}
3.2.3 其他模块的实现
其他模块Repository以及Retrofit的定义可以参考上面协程在网络中的使用讲解。这里的重点是演示如何使用applicationScope。
3.2.4 在UI控制器(Activity或者Fragment)中使用
上述的的需求场景我们通常在onDestory()中调用reportData,如下:
override fun onDestroy() {
myViewModel.reportData()
super.onDestroy()
}
四、取消单个协程
一般开发中,大部分业务场景使用viewModelScope或者lifeCycleScope来启动协程,并由他们自动控制所启动协程的生命周期。但是有时也需要单独管理我们启动的协程,比如登录的过程中弹出了一个Loading框,用户按了返回按钮取消了登录操作,那么我们就需要取消该协程。否则服务端返回正确的数据后会执行我们正常的业务流程。那么使用协程该如何解决这种场景呢?可参考下面的方式进行控制:
private var loginJob: Job? = null
private fun login(){
//需要loginViewModel.login()返回一个协程的句柄
loginJob = loginViewModel.login()
}
//取消登录协程的后续操作
private fun cancelLogin(){
loginJob?.cancel()
}
LoginViewModel参考示例
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
fun login():Job{
return viewModelScope.launch{
loginRepository.login()
}
}
}
最后:
- 协程帮我们简化了异步并发代码的编写方式,可用同步的方式写异步代码
- 从上面的使用示例可以看出,使用协程方便了Android上的线程切换操作,代码层面减少了很多回调函数。
通过上面的应该场景,了解了协程的基本使用招式,但是背后的实现逻辑是什么呢?后续我们进一步探讨。
参考:
Android官网的协程指南
|