通勤路上学协程
作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。
前言
此前,在落地WebView离线加载的时候,需要做离线资源包的更新管理。此间涉及许多接口请求和文件资源下载的逻辑,会有频繁的线程切换场景。因此,在这样的需求下,尝试使用了「协程」来作为「线程切换工具」。
什么是「协程」
什么是协程?Google了一下,大多数大佬都说是轻量级线程。心想,这么神奇?在JVM体系下竟然还有比线程颗粒度更小的吗?带着这个疑问,又看了一圈,最后在扔物线大佬的文章中有详细的说明了「协程」和「线程」的关系。这里贴一下
当我们讨论协程和线程的关系时,很容易陷入中文的误区,两者都有一个「程」字,就觉得有关系,其实就英文而言,Coroutines 和 Threads 就是两个概念。 从 Android 开发者的角度去理解它们的关系:
- 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
- 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
- 单线程中的协程总的执行时间并不会比不用协程少。
- Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。
- 协程设计的初衷是为了解决并发问题,让 「协作式多任务」 实现起来更加方便
这里解释了协程与线程的关系。总而言之,在Android开发中,是一个让开发者以"同步的代码风格来实现异步逻辑"的工具。
如何使用「协程」
小试牛刀-配置
groovy
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
...
}
小试牛刀-开启协程
开启一个协程,使用scope的launch函数。
kotlin
MainScope().launch {
doSomethingInMainThread(); //主线程
}
小试牛刀-切换线程
使用withContext,可以指定线程
kotlin
MainScope().launch {
doSomethingInMainThread(); //主线程
withContext(Dispatchers.IO) {
doSomethingIoThread(); //子线程
}
}
这里可以说明,Dispatchers
有几个类型提供使用,可以根据业务场景使用不同的Dispatcher
- Dispatchers.Default,计算密集型子线程
- Dispatchers.Main,主线程
- Dispatchers.IO,IO密集型子线程
小试牛刀-suspend
suspend
关键字是kotlin提供给协程的关键字,中文意思是「暂停」或者是「可挂起」的意思。但其实,给方法加suspend
关键字,是不会给方法施加魔法让其变成「可挂起」方法的。而这个作用其实是传递的作用。怎么理解,个人的理解是类似"抛异常"这种传递的关系。
比如:
kotlin
suspend fun methodA() {}
//此处不加suspend会编译不过,提示:
//Suspend function 'methodA' should be called only from a coroutine or another suspend function
suspend fun methodB() {
methodA();
}
从上述提示,就可看出,要么使用suspend
关键字传递下去,要么使用开启协程launch
调用suspend方法。这跟"异常"处理是不是比较相似,要么try-catch处理了,要么给方法继续抛异常~
上手简单实战!
小试牛刀了三板斧后,也可以开始上手实战了,这里以此前项目中资源管理的例子,给大家展示一波~
先解释一下业务流程:
- 下载资源文件
- 下载清单文件(包含资源的一些md5和版本信息)
- 校验资源文件
- 更新资源到本地目录
上代码!(这里篇幅原因,会使用伪代码进行展示,各位大佬毕竟对于下载和各种IO操作都已经溜得飞起了)
kotlin
//下载文件
fun downloadFile(String url) {
val request = Request.Builder().url(url).build()
val response = okHttpClient.newCall(request).execute() //⚠️这里使用OkHttp的同步请求方式
val file = parse(response) //解析请求响应
return file
}
--------------分割线,划重点--------------
//下载清单文件,suspend方法,调度到IO线程中执行下载
suspend fun downloadManifest(String manifestUrl) {
return withContext(Dispatchers.IO) {
return downloadFile(manifestUrl)
}
}
//下载资源文件,suspend方法,调度到IO线程中执行下载
suspend fun downloadRes(String resUrl) {
return withContext(Dispatchers.IO) {
return downloadFile(resUrl)
}
}
//校验资源文件
suspend fun verifyRes(resFile : File, manifestFile: File) : Boolean {
return withContext(Dispatchers.IO) {
String manifest = FileUtils.read(manifestFile) //IO读文件
val resFileList : List<File> = unzip(resFile);
var flag = true
resFileList.forEach { it ->
if(!verify(it , manifest)) { //校验文件
flag = false
}
}
return flag
}
}
//更新资源到本地目录
suspend fun updateLocalRes(resFile : File) : Boolean {
return withContext(Dispatchers.IO) {
deleteLocalPath() // 删除本地资源
saveLocalPath(resFile) //从下载目录挪到本地资源路径
deleteCache() // 删除下载目录的文件
}
}
fun processDownload() {
MainScope().launch {
val manifestFile = downloadManifest(manifestUrl)
val resFile = downloadRes(resUrl)
if(!verifyRes(resFile, manifestFile)) { //🎉这里等待两者下载完成后才执行校验逻辑
updateLocalRes()
}
}
}
这样,一个下载-校验-更新的流程就清爽的完成了,全程没有回调!看上去十分简洁。换了以前,至少也得需要三四层的回调嵌套,大大提高了代码的可读性~至此,已经算是入门了协程的门了,但是想要用好协程,我们还需要多学习一些api和原理。比如各种scope
(lifecycleScope
,GlobalScope
等),还有async
等挂起函数。甚至探究协程是如何实现这种"同步写异的魔法的。后面会出文章跟大家一起学习,敬请期待~