写点什么

史上最详 Android 版 kotlin 协程入门进阶实战(一),一线互联网公司面经总结

用户头像
Android架构
关注
发布于: 15 小时前

public interface Deferred<out T> : Job {


//返回结果值,或者如果延迟被取消,则抛出相应的异常


public suspend fun await(): T


public val onAwait: SelectClause1<T>


public fun getCompleted(): T


public fun getCompletionExceptionOrNull(): Throwable?


}


我们需要重点关注await()方法,可以看到await()方法返回结果是T,说明我们可以通过await()方法获取执行流的返回值,当然如果出现异常或者被取消执行,则会抛出相对应的异常。

[](

)什么是作用域


协程作用域(Coroutine Scope)是协程运行的作用范围。launchasync都是CoroutineScope的[扩展函数](


),CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。就好比变量的作用域,如下面test方法中的money变量


private fun test(){ // scope start


int money = 100;


println(money)


} // scope end


// println(money)


此时money是不能被调用,因为 AS 会提示 Unresolved reference: money。协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。我们开发过程中最常见的场景就内存泄露,协程同样存在这样的问题,后面我们再细细讲解协程作用域CoroutineScope的相关知识,这里只是作为基础点讲解,不继续往下延伸。


[](


)Kotlin 协程的基础用法


============================================================================


现在我们开始使用协程,首先我们在MainActivity的 xml 布局中新建一个Button按钮然后设置好点击事件,然后创建一个start()方法,通过Button的点击事件执行。现在我们开始在start方法中使用协程。


刚才我们上面提到启动协程有三种方式,接下来我们先看看如何通过runBlockinglaunchasync启动协程,我们直接在 start 方法中使用,但是由于我们的launchasync启动,只能在协程的作用域下启动,那我们又该怎么办呢?


[](


)运行第一个协程




在 Android 中有一个名为GlobalScope全局顶级协程,这个协程是在整个应用程序生命周期内运行的。我们就以此协程来使用launchasync启动,代码如下:


import android.os.Bundle


import android.util.Log


import android.view.View


import android.widget.Button


import android.widget.LinearLayout


import androidx.appcompat.app.AppCompatActivity


import androidx.constraintlayout.widget.Group


import androidx.viewpager.widget.ViewPager


import kotlinx.coroutines.*


import java.lang.NullPointerException


class MainActivity : AppCompatActivity() {


private lateinit var btn:Button


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


setContentView(R.layout.activity_main)


btn = findViewById(R.id.btn)


btn.setOnClickListener {


start()


}


}


private fun star


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


t(){


runBlocking {


Log.d("runBlocking", "启动一个协程")


}


GlobalScope.launch{


Log.d("launch", "启动一个协程")


}


GlobalScope.async{


Log.d("async", "启动一个协程")


}


}


}


然后运行 app,点击按钮执行start()方法。我们就可以在控制台上看到如下输出:


D/runBlocking: 启动一个协程


D/launch: 启动一个协程


D/async: 启动一个协程




,so easy。协程原来这么简单,那我们接着继续往下走。上面提到过三种启动方式分别会的得到各自的返回信息。我们现在增加三个变量然后分别用协程进行赋值,同时进行输出:


private fun start(){


val runBlockingJob = runBlocking {


Log.d("runBlocking", "启动一个协程")


}


Log.d("runBlockingJob", "$runBlockingJob")


val launchJob = GlobalScope.launch{


Log.d("launch", "启动一个协程")


}


Log.d("launchJob", "$launchJob")


val asyncJob = GlobalScope.async{


Log.d("async", "启动一个协程")


"我是返回值"


}


Log.d("asyncJob", "$asyncJob")


}


然后运行,我们可以在控制台上看到如下输出:


D/runBlocking: 启动一个协程


D/runBlockingJob: 41


D/launchJob: StandaloneCoroutine{Active}@3b8b871


D/launch: 启动一个协程


D/async: 启动一个协程


D/asyncJob: DeferredCoroutine{Active}@63f265


也有可能是


D/runBlocking: 启动一个协程


D/runBlockingJob: 41


D/launchJob: StandaloneCoroutine{Active}@1344515


D/asyncJob: DeferredCoroutine{Active}@38c002a


D/async: 启动一个协程


D/launch: 启动一个协程


还有可能是


D/runBlocking: 启动一个协程


D/runBlockingJob: 41


D/launch: 启动一个协程


D/launchJob: StandaloneCoroutine{Active}@b94e973


D/async: 启动一个协程


D/ asyncJob: DeferredCoroutine{Active}@f7aa030


嗯哼,什么情况


怎么后面 4 条日志顺序还是随机的。没有看懂的童鞋,说明你没有仔细看上面的文字。


知识点来了,赶紧拿出你的小本本记下来,我们一个一个的来分析。


我们在上面提到过runBlocking启动的是一个新的协程并阻塞调用它的线程,我们对比输出日志可以看到前两条runBlocking的相关输出日志的位置顺序是不会变化的,这就证明我们之前所说的runBlocking会阻塞调用它的线程,直到runBlocking运行结束才继续往下执行。


接下来我们再继续往下看,我们看到后面四条日志是无序的,但是launchJob始终在asyncJob前面。而launchasync协程体内的日志输出是无序的。每执行一次看到的顺序都有可能跟之前的不一样。我们前面提到过launchasync都是启动一个协程但不会阻塞调用线程,所以launchJob始终在asyncJob前面(2 个协程之间不是很明显,你们自己在尝试的时候,可以同时启动 5 个甚至更多协程去看日志输出)


launchasync协程体内的日志是无序的,这是因为协程采用的是并发设计模式,所以launchasync的协程体内的 log 日志输出是无序方式,这就解释了launchasync都是启动一个协程但不会阻塞调用线程,同时也解释了 log 日志之间输出顺序之间的关系(这里描述是不严谨的,后面会补充)。


难道就这样结束了吗?那你想的可就太简单了。刚才我们提到协程采用的是并发设计模式,多个协程并发执行的。那如果这个时候,我们把启动协程放在同一协程作用域下启动的是顺序又该是怎么样的呢? 大家可以先思考一下这个问题,回头我们再来看这个问题。


[](


)runBlocking 的返回值




现在我们回到之前的话题,我们看到输出的日志信息中runBlockingJob的输出结果是 41,为什么是这么一个数值,其实他默认返回是一个该协程作业的当前状态



我们通过runBlocking方法可以看到,其返回值是调用了joinBlocking方法,而在joinBlocking方法中



我们看到joinBlocking方法返回了一个 state 强转成泛型T类型。我们现在大概知道runBlocking返回的是个什么东西了。如果在runBlocking协程最后一行增加一个返回值:


val runBlockingJob = runBlocking {


Log.d("Coroutine", "runBlocking 启动一个协程")


"我是 runBlockingJob 协程的返回值"


}


我们将会看到如下输出:


D/Coroutine: runBlocking 启动一个协程


D/runBlockingJob: 我是 runBlockingJob 协程的返回值


runBlocking它的设计目的是将常规的阻塞代码连接到一起,主要用于main函数和测试中。根据本文章的目标我们后续将不再往下扩展。


继续往下走,我们看到launchJob输出的是一个StandaloneCoroutine对象,为什么会是一个StandaloneCoroutine对象呢,不是说好的返回一个 Job 吗?


别慌,稳住!继续往下看


[](


)launch 函数





我们看到launch函数中有 3 个参数contextstartblock,同时都带有默认值,虽然我们不知道这三个参数是干什么用的,但是我们可以看名知其意,不妨先大胆的猜测一下,我们这里先跳过,后面再针对这三个参数做一些基本讲解。我们看到launch方法最终返回的是一个coroutine对象,由于我们没有传入值其最后返回的是一个StandaloneCoroutine对象,跟我们输出的日志结果一致。那为什么笔者会说launch返回的是一个 Job 呢。我们再继续看看StandaloneCoroutine又是一个什么鬼,通过查找继承关系我们可以看到,StandaloneCoroutine就是一个 Job,现在就一目了然了。


private open class StandaloneCoroutine(...) : AbstractCoroutine<Unit>(parentContext, active){


//此处省略.....


}


public abstract class AbstractCoroutine<in T>(...) : JobSupport(active), Job, Continuation<T>, CoroutineScope {


//此处省略.....


}


[](


)async 函数




同理我们也看看async函数,和launch拥有同样的 3 个参数contextstartblock,默认值都是一样的,最终返回的是也是一个coroutine对象。只是async返回的DeferredCoroutine对象。



private open class DeferredCoroutine<T>(...) : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T> {


//此处省略.....


}


同样的都是继承 AbstractCoroutine<Unit>类,但是DeferredCoroutine同时也继承Deferred<T>接口。这么看来DeferredCoroutine就是一个Deferred<T>,一个携带有返回值Job。那么问题来了,我们要怎么获取到这个Deferred<T>携带的返回值T呢。


我们在一开始的时候提到需要重点关注Deferredawait()方法,我们可以通过返回Deferred对象,调用await()方法来获取返回值,我们看到await()前面有个suspend关键字,这又是个额啥玩意。


public suspend fun await(): T


[](


)挂起函数




suspend是协程的关键字,表示这个一个挂起函数,每一个被suspend饰的方法只能在suspend方法或者在协程中调用。现在我们修改之前的代码,同时多增加几条输出日志:


private fun start(){


GlobalScope.launch{


val launchJob = launch{


Log.d("launch", "启动一个协程")


}


Log.d("launchJob", "$launchJob")


val asyncJob = async{


Log.d("async", "启动一个协程")


"我是 async 返回值"


}


Log.d("asyncJob.await", ":${asyncJob.await()}")


Log.d("asyncJob", "$asyncJob")


}


}


现在我们通过GlobalScope.launch启动里一个协程,同时在协程体里面通过launch直接又启动了 2 个协程。为什么我们没有在协程体使用GlobalScope.launch启动,而是使用launch直接启动。前面我们提到过调用launch必须要在协程作用域(Coroutine Scope)中才能调用,因为通过runBlockinglaunchasync启动的协程体等同于协程作用域,所以这里我们就可以直接使用launch启动一个协程。我们运行一下,接着看看日志输出:


D/launchJob: StandaloneCoroutine{Active}@f3d8da3


D/launch: 启动一个协程


D/async: 启动一个协程


D/await: :我是 async 返回值


D/asyncJob: DeferredCoroutine{Completed}@d6f28a0


也有可能是这样的


D/launchJob: StandaloneCoroutine{Active}@f3d8da3


D/async: 启动一个协程


D/launch: 启动一个协程


D/asyncJob.await: :我是 async 返回值


D/asyncJob: DeferredCoroutine{Completed}@d6f28a0


现在我们看到asyncJob.await也是输出我们之前定义好的返回值,同时DeferredCoroutine的状态变成了{Completed},这是因为await()是在不阻塞线程的情况下等待该值的完成并继续执行,当deferred计算完成后返回结果值,或者如果deferred被取消,则抛出相应的异常CancellationException。但是又因为await()是挂起函数,他会挂起调用他的协程。所以我们看到的DeferredCoroutine的状态是{Completed},同时输出的await日志也是在最后面。


好了,至此。我们对runBlockinglaunchasync的相关介绍就到这里了。


[](


)Android 中的协程并发与同步




现在我们回过头来看,我们在上面提到过:“因为协程采用的是并发设计模式,所以导致launchasync的协程体内的 log 日志输出是无序方式(这样说是不严谨)”。


因为协程是采用就是并发的设计模式,这句话的大多数环境下是没有问题。但是,但是,但是,这里需要注意的小细节来了。如果某个协程满足以下几点,那它里面的子协程将会是同步执行的:


  • 父协程的协程调度器是处于Dispatchers.Main情况下启动。

  • 同时子协程在不修改协程调度器下的情况下启动。


private fun start() {


GlobalScope.launch(Dispatchers.Main) {


for (index in 1 until 10) {


//同步执行


launch {


Log.d("launch$index", "启动一个协程")


}


}


}


}


D/launch1: 启动一个协程


D/launch2: 启动一个协程


D/launch3: 启动一个协程


D/launch4: 启动一个协程


D/launch5: 启动一个协程


D/launch6: 启动一个协程


D/launch7: 启动一个协程


D/launch8: 启动一个协程


D/launch9: 启动一个协程


private fun start() {


GlobalScope.launch {


for (index in 1 until 10) {


//并发执行


launch {


Log.d("launch$index", "启动一个协程")


}


}


}


}


D/launch1: 启动一个协程


D/launch2: 启动一个协程


D/launch3: 启动一个协程


D/launch4: 启动一个协程


D/launch5: 启动一个协程


D/launch6: 启动一个协程


D/launch9: 启动一个协程


D/launch7: 启动一个协程


D/launch8: 启动一个协程


那么子协程将是同步执行的,这是在 Android 平台上如果协程处于Dispatchers.Main调度器,它会将协程调度到 UI 事件循环中执行,即通常在主线程上执行,这样就能理解为什么是同步执行了吧。如果是不同步的话,那我在操作 UI 刷新的时候,就会出现各种问题啦!


如果其中的某一个子协程将他的协程调度器修改为非Dispatchers.Main,那么这个子协程将会与其他子协程并发执行,这里我们就不在演示,各位看官可以自己动手试验一下。毕竟知识光看不动手,是很难将知识吸收到位的。


下一本章节我们将在会对以下知识点做初步讲解,包含上文提到的launchasync函数中的 3 个参数作用。清单如下:


  1. 协程调度器CoroutineDispatcher

  2. 协程下上文CoroutineContext作用

  3. 协程启动模式CoroutineStart

  4. 协程作用域CoroutineScope

  5. 挂起函数以及suspend关键字的作用




如果你想学习 kotlin 又缺少学习资料,我正好薅到这本谷歌内部大佬根据实战编写的 Kotlin 宝典,从入门到精通,教程通俗易懂,实例丰富,既有基础知识,也有进阶技能,能够帮助读者快速入门,是你学习 Kotlin 的葵花宝典,快收藏起来!!!



今天分享的一共分为两部分:【字节跳动厂内部超高质量 Kotlin 笔记】、【谷歌大佬编写高级 Kotlin 强化实战(附 Demo)】。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
史上最详Android版kotlin协程入门进阶实战(一),一线互联网公司面经总结