安卓-入门kotlin协程

avatar

作者

大家好,我叫小琪;

本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队;

目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。

一些概念

在了解协程之前,我们先回顾一下线程、进程的概念

img

1.进程:拥有代码和打开的文件资源、数据资源、独立的内存空间,是资源分配的最小单位。

2.线程:从属于进程,是程序的实际执行者,一个进程至少包含一个线程,操作系统调度(CPU调度)执行的最小单位

3.协程:

  • 不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行
  • 进程、线程是操作系统维度的,协程是语言维度的。

协程特点

  • 异步代码同步化

下面通过一个例子来体验kotlin中协程的这一特点

有这样一个场景,请求一个网络接口,用于获取用户信息而后更新UI,将用户信息展示,用kotlin的协程这样写:

GlobalScope.launch(Dispatchers.Main) {   // 在主线程开启协程
    val user = api.getUser() // IO 线程执行网络请求
    tvName.text = user.name  // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                tvName.setText(user.name);
            }
        })
    }
    
    @Override
    public void failure(Exception e) {
        ...
    }
});

java中的这种异步回调打乱了正常代码顺序,虽说保证了逻辑上是顺序执行的,但使得阅读相当难受,如果并发的场景再多一些,将会出现“回调地狱”,而使用了 Kotlin 协程,多层网络请求只需要这么写:

GlobalScope.launch(Dispatchers.Main) {       // 开始协程:主线程
    val token = api.getToken()                  // 网络请求:IO 线程
    val user = api.getUser(token)               // 网络请求:IO 线程
    tvName.text = user.name                     // 更新 UI:主线程
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码

协程初体验

1.引入依赖

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

2.第一个协程程序

布局中添加一个button,并为它设置点击事件

btn.setOnClickListener {
    Log.i("TAG","1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
    CoroutineScope(Dispatchers.Main).launch{
        delay(1000)     //延迟1000ms
        Log.i("TAG","2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
    }
    Log.i("TAG","3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程....[当前线程为:main]
3.BtnClick.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]

通过CoroutineScope.launch方法开启了一个协程,launch后面花括号内的代码就是运行在协程内的代码。协程启动后,协程体里的任务就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来

进入到launch方法看看它里面的参数,

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
}

对这些参数的说明:

  • context:协程上下文,可以指定协程限制在一个特定的线程执行。常用的有Dispatchers.Default、Dispatchers.Main、Dispatchers.IO等。Dispatchers.Main即Android 中的主线程;Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • start: 协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT指协程立即执行,另外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED
  • block:协程主体,即要在协程内部运行的代码,也就是上述例子花括号中的代码
  • 返回值Job:对当前创建的协程的引用。可以通过调用它的的join、cancel等方法来控制协程的启动和取消。

3.挂起函数

上面有提到”挂起“即suspend的概念,

回到上面的例子,有一个delay函数,进到这个函数看看它的定义:

public suspend fun delay(timeMillis: Long) {...}

发现多了个suspend关键字,也就是上文中提到的“挂起”,根据程序的输出结果看,首先输出了1,3,等待一秒后再输出了2,而且打印的线程显示的也是主线程,这说明,协程在遇到suspend关键字的时候,会被挂起,所谓的挂起,就是程序切了个线程,并且当这个挂起函数执行完毕后又会自动切回来,这个切回来的动作其实就是恢复,因此挂起、恢复也是协程的一个特点。所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过这个挂起恢复机制进行线程的切换。

关于suspend函数也有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。

4.创建协程的其他方式

上面介绍了通过launch方法创建协程,当遇到 suspend 函数的时候 ,该协程会自动逃离当前所在的线程执行任务,此时原来协程所在的线程就继续干自己的事,等到协程的suspend 函数执行完成后又自动切回来原来线程继续往下走。 但如果协程所在的线程已经运行结束了,协程还没执行完成就不会继续执行了 。为了避免这样的情况就需要结合 runBlocking 来暂时阻塞当前线程,保证代码的执行顺序。

下面我们通过runBlocking 来创建协程

btn.setOnClickListener {
            Log.i("TAG", "1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
            runBlocking {
                delay(1000)     //延迟1000ms
                Log.i("TAG", "2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
            }
            Log.i("TAG", "3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
        }

执行结果如下:

1.准备启动协程.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]
3.BtnClick.... [当前线程为:main]

可以看到运行结果顺序和上面的launch方式不同,这里的log先输出1、2,再输出3,程序会等待runBlocking中的代码块执行完后才会还执行后面的代码,因此launch是非阻塞的,而runBlocking是阻塞式的。

launch和runBlocking都是没有返回结果的,有时我们想知道协程的返回结果,拿到结果去做业务例如UI更新,这时withContext和async就派上用场了。

先看下withContext的使用场景:

 btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val startTime = System.currentTimeMillis()
                val task1 = withContext(Dispatchers.IO) {
                    delay(2000)
                    Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                    1  //返回结果赋值给task1
                }

                val task2 = withContext(Dispatchers.IO) {
                    delay(1000)
                    Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                    2 //返回结果赋值给task2
                }
                Log.i(
                    "TAG",
                    "3.计算task1+task2 = ${task1+task2}  , 耗时 ${System.currentTimeMillis() - startTime} ms  [当前线程为:${Thread.currentThread().name}]"
                )
            }
        }

输出结果为:

 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
 2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
 3.计算 task1+task2 = 3  , 耗时 3032 ms  [当前线程为:main]

从输出结果可以看出,通过withContext指定协程运行在一个io线程,延迟了两秒后返回结果1赋值给task1,之后程序向下执行,同样的,延迟了1s后返回结果2赋值给了task2,最后执行到步骤三,并且打印了耗时时间,可以看到,耗时是两个task的时间总和,也就是先执行完task1,在执行task到,说明withContext是串行执行的,这适用于在一个请求结果依赖另一个请求结果的场景。

如果同时处理多个耗时任务,且这几个任务都没有相互依赖时,可以使用 async ... await() 来处理,将上面的例子改为 async 来实现如下

btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val startTime = System.currentTimeMillis()
                val task1 = async(Dispatchers.IO) {
                    delay(2000)
                    Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                    1  //返回结果赋值给task1
                }

                val task2 = async(Dispatchers.IO) {
                    delay(1000)
                    Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                    2 //返回结果赋值给task2
                }

                Log.i(
                    "TAG",
                    "3.计算 task1+task2 = ${task1.await()+task2.await()}  , 耗时 ${System.currentTimeMillis() - startTime} ms  [当前线程为:${Thread.currentThread().name}]"
                )
            }
        }

输出结果:

2.执行task2.... [当前线程为:DefaultDispatcher-worker-4]
1.执行task1.... [当前线程为:DefaultDispatcher-worker-5]
3.计算 task1+task2 = 3  , 耗时 2010 ms  [当前线程为:main]

可以看到,输出的总耗时明显比withContext更短,且task2优先task1执行完,说明async 是并行执行的。

总结

本文首先通过对进程、线程、协程的区别认清协程的概念,接着对协程的特点也就是优势进行了介绍,最后通过几个实例介绍了协程的几种启动方式,并分析了其各自特点和使用场景,本文更多是对协程的概念和使用进行了简单的介绍,而协程的内容远不止这些。

结束语

过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;

企业微信截图_5d79a123-2e31-42cc-b03f-9312b8b99df3.png