阅读 466

【疯狂Android之Kotlin】 简单理解与使用Kotlin中的协程

协程的简介

  • 从面试角度考查对协程的了解:
  1. 协程是轻量级的线程,为什么是轻量的?可以先告诉大家结论,因为它基于线程池API,所以在处理并发任务这件事上它真的游刃有余。
  2. 有可能有的同学问了,既然它基于线程池,那我直接使用线程池或者使用 Android 中其他的异步任务解决方式,比如Handler、RxJava等,不更好吗?(这里值得各位同学思考一番)
  3. 协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱,这是其最大的优点。
  • 从 Android 开发者的角度去理解它们的关系:
  1. 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
  2. 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
  3. 单线程中的协程总的执行时间并不会比不用协程少。
  4. Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。
  5. 我们学习Kotlin中的协程,一开始确实可以从线程控制的角度来切入。因为在 Kotlin 中,协程的一个典型的使用场景就是线程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

初识协程

  • 首先肯定要知道,所谓的Kotlin协程到底长啥样,以下同学我引用了(copy)了官网的一个例子:
fun main(args: Array<String>) {
    launch(CommonPool) {
        delay(1000L) 
        println("World!") 
    }
    println("Hello,")
    Thread.sleep(2000L)
}

/* 
运行结果: ("Hello,"会立即被打印, 1000毫秒之后, "World!"会被打印)
Hello, 
World!
*/
复制代码

姑且不管里面具体的细节, 上面代码大体的运行流程是这样的:

  1. 主流程:

调用系统的launch方法启动了一个协程, 跟随的大括号可以看做是协程体. (其中的CommonPool暂且理解成线程池, 指定了协程在哪里运行) 打印出"Hello," 主线程sleep两秒 (这里的sleep只是保持进程存活, 目的是为了等待协程执行完)

  1. 协程流程:
  • 协程延时1秒
  • 打印出"World!"

解释一下delay方法: 在协程里delay方法作用等同于线程里的sleep, 都是休息一段时间, 但不同的是delay不会阻塞当前线程, 而像是设置了一个闹钟, 在闹钟未响之前, 运行该协程的线程可以被安排做了别的事情, 当闹钟响起时, 协程就会恢复运行.

协程启动后还可以消失

  • 在launche方法中有一个返回值,类型是Job,Job有一个cancel方法可以取消协程,下面可以看看一个数羊的简单例子
fun main(args: Array<String>) {
    val job = launch(CommonPool) {
        var i = 1
        while(true) {
            println("$i little sheep")
            ++i
            delay(500L)  // 每半秒数一只, 一秒可以输两只
        }
    }

    Thread.sleep(1000L)  // 在主线程睡眠期间, 协程里已经数了两只羊
    job.cancel()  // 协程才数了两只羊, 就被取消了
    Thread.sleep(1000L)
    println("main process finished.")
}
复制代码
  • 它的运行结果是:
1 little sheep
2 little sheep
main process finished.
复制代码

以上只是简单介绍了下Kotlin协程的使用,具体可以参考以下资料,进行深入了解

Android中引用协程

第一步:引入库,Android 需要引入如下两个库

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
复制代码

声明协程作用域CoroutineScope

这里建议有生命周期的类继承 CoroutineSocpe,这样就能让全部协程跟着生命周期结束

  • 如在activity里面使用
MainActivity : AppCompatActivity(), CoroutineScope by MainScope(){
    override fun onDestroy(){
        super.onDestory()
        cancel()
     }
}  
复制代码
  • 在UI逻辑类中使用
class MainActivityFacede : CoroutineScope {
    private val job = Job()

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    fun destroy() {
        job.cancel()
    }
}
复制代码

以上代码会在调用destroy的时候取消这个作用域中的协程运行

运行协程

  1. 那么我这里简单说两种运行协程,launch与async 。

因为我们是说快速使用,所以我这里也不跟大家扣源码了。(小声哔哔,源码看得头疼)

  1. 简单说来,它们之间的区别如下:
  • launch 没有返回值,或者说返回只是 job ,能够知道任务的状态,却不能携带返回结果。
  • async 有返回值,也就是返回的是 Deferred ,它是继承的 job ,所有job有的,它都有,还具备了job没有的携带数据回来的能力。
  • launch 可以用来运行不需要操作结果的协程(如文件删除,创建等)
  • async 可以用来运行异步耗时任务并且需要返回值的任务(网络请求,数据库操作,文件读写等)。
  1. 简单补充一下Job和Deferred的生命周期
StateisActiveisConpletedisCanceled
newfalsefalsefalse
activetruefalsefalse
completingtruefalsefalse
cancelingfalsetruetrue
completedfalsetruefalse

使用方式

  • 方式一
private suspend fun getWebTime(): Long = withContext(Dispatchers.IO) {
    var result = RequeastTest.getInstance().start()
    val name = Thread.currentThread().name
    if (!coroutines.contains(name)) {
        coroutines.add(name)
    }
    result
}

launch() {
    //do sth
    var time = getWebTime()
    //update UI
}
launch {
    var deferred = async() {
        //发起网络请求..
        getWebTime()
    }
    //do sth ...
    var value = deferred.await()
    //do sth...
}
复制代码
  • 方式二
private suspend fun getWebTime(): Long {
    var result = RequeastTest.getInstance().start()
    val name = Thread.currentThread().name
    if (!coroutines.contains(name)) {
        coroutines.add(name)
    }
    return result
}

launch() {
    //do sth
    var time = withContext(Dispather.IO){
        getWebTime()
    }
    //update UI
    
}
launch {
    var deferred = async(Dispather.IO) {
        //发起网络请求..
        getWebTime()
    }
    //do sth ...
    var value = deferred.await()
    //do sth...
}
复制代码

从以上两个方式中,可以看到使用了新的东西Dispather和suspend。

  1. 说说Dispther
  • 可以理解为协程调度器,可以用来调度协程跑到哪个线程中
  • 以下是取值的说明
说明
Dispathers.Default共享后台线程池里的线程
Dispathers.MainAndroidUi线程
Dispathers.IO共享后台线程池的线程
Dispthers.Uniconfined不限制,使用父协程的线程
newSingleThreadContext使用新的线程
  • Dispather 可以在 launch、async 等启动协程时,指定在哪个线程里面运行,也可以在协程中,使用 withContext(Dispather.)

来切换线程,使用 withContext 切换线程时,有返回值。

  1. Suspend
  • 协程里面唯一一个修饰符,用来修改函数的,表明函数是一个挂起函数,协程编译器会在编译期间进行CPS变换,去做一些不可描述的事情(具体怎么个不可描述的事情,可以参考官方文档,此处暂不详解)。
  • 用suspend修饰的函数,只能在协程体和同样使用 suspend 修饰的函数中调用。

以上两个的区别在于 getWebTime 的函数写法,也就造成协程的写法有所区别(调度线程的写法区别)。

  • 另外其实可以需要同时进行多个网络请求,并在全部请求完毕之后进行数据整理,渲染的时候,async 比什么都方便,如下:
launch {
    var userInfoDeferred = async {
        //获取用户基本信息
        getUserInfo(aid)
    }

    var userTeamsDeferred = async{
        //获取用户团队..
        getUserTeams(aid)
    }
    
    var userOrgsDeferred =  async {
        //获取用户组织机构
        getUserOrgs(aid)
    }
    
    var userInfo = userInfoDeferred.await()
    var userTeams = userTeamsDeferred.await()
    var userOrgsDeferred = userOrgsDeferred.await()
    //渲染UI 
}
复制代码

ps: 如果需要详细了解,请移步官网文档

总结

  • 以上总结了对于协程的简单理解与使用,协程到底是什么, 很难给出具体的定义, 就算能给出具体定义, 也会非常抽象难以理解的.
  • 另一方面, 协程可以说是编译器的能力, 因为协程并不需要操作系统和硬件的支持(线程需要), 是编译器为了让开发者写代码更简单方便, 提供了一些关键字, 并在内部自动生成了一些支持型代码(可能是字节码).
  1. 以下是我个人对协程的总结:
  • 首先, 协程是一片包含特定逻辑的代码块, 这个代码块可以调度到不同的线程上执行;
  • 其次, 协程一种环境, 在这种环境里, 方法可以被等待执行, 有了运算结果之后才返回, 在等待期间, 承载协程的线程资源可以被别的地方使用.
  • 第三, 协程是一个独立于运行流程的逻辑流程, 协程里面的步骤, 无论是同步的还是异步的, 都是线性(从前到后依次完成的).

ps:有什么纰漏的地方,欢迎各位同学指出