作者

大家好,我叫小琪;

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

目前首要担任国内发行安卓相关开发,一起兼顾内部几款App开发。

一些概念

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

安卓-入门kotlin协程

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 是并行履行的。

总结

本文首先经过对进程、线程、协程的差异认清协程的概念,接着对协程的特色也便是优势进行了介绍,最后经过几个实例介绍了协程的几种发动方法,并分析了其各自特色和运用场景,本文更多是对协程的概念和运用进行了简单的介绍,而协程的内容远不止这些。

完毕语

进程中有问题或许需求沟通的同学,能够扫描二维码加好友,然后进群进行问题和技术的沟通等;

安卓-入门kotlin协程