Kotlin 协程入门这一篇就够了

栏目: Android · 发布时间: 4年前

内容简介:协程通过替代回调(callback)来简化异步代码听起来蛮抽象的,来看代码Android系统为了保证界面的流畅和及时响应用户的输入事件,主线程需要保持每16ms一次的刷新(调用

协程通过替代回调(callback)来简化异步代码

听起来蛮抽象的,来看代码

fun fetchDocs() {
   		val result = get("developer.android.com")
   		show(result)
	}
复制代码
Kotlin 协程入门这一篇就够了

Android系统为了保证界面的流畅和及时响应用户的输入事件,主线程需要保持每16ms一次的刷新(调用 onDraw() 函数),所以不能在主线程中做耗时的操作(比如 读写数据库,读写文件,做网络请求,解析较大的 Json 文件,处理较大的 list 数据)。

get() 通过接口获取用户数据,如果在主线程中调用 fetchDocs() 函数就会阻塞(block)主线程,App 会卡顿甚至崩溃。

所以需要在子线程中调用 get() 函数,这样主线程就可以刷新界面和处理用户输入,待 get() 函数执行完毕后通过 callback 拿到结果。

fun fetchDocs() {
        get("developer.android.com") { result ->
            show(result)
        }
    }
复制代码
Kotlin 协程入门这一篇就够了

callback 是个不错的方式,但是 callback 被过度使用后代码可读性会变差(迷之缩进),而且 callback 不能使用 exception。为了解决这样的问题,欢迎协程(coroutine)闪亮登场

suspend fun fetchDocs() {
        val result = get("developer.android.com")
        show(result)
    }

    suspend fun get(url: String) =
            withContext(Dispatchers.IO) {
                ...
            }
复制代码
Kotlin 协程入门这一篇就够了

明明是同步的写法为什么不会阻塞主线程? 对,因为 suspend

suspend 修饰的函数比普通函数多两个操作(suspend 和 resume)

  • suspend:暂停当前协程的执行,保存所有的局部变量
  • resume:从协程被暂停的地方继续执行协程
Kotlin 协程入门这一篇就够了

get() 函数同样也是一个 suspend 函数。*** suspend 修饰的函数并不意味着运行在子线程中***,如果需要指定协程运行的线程,就需要指定Dispatchers ,常用的有三种:

  • Dispatchers.Main:Android中的主线程,可以直接操作UI
  • Dispatchers.IO:针对磁盘和网络IO进行了优化,适合IO密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合CPU密集型的任务,比如解析JSON文件,排序一个较大的list

通过 withContext() 可以指定Dispatchers,这里的 get() 函数里的 withContext 代码块中指定了协程运行在Dispatchers.IO中。

来看下这段代码的具体执行流程

Kotlin 协程入门这一篇就够了
  • 每个线程有一个调用栈(call stack), Kotlin使用它来追踪哪个函数在执行和它的局部变量
  • 当调用到 suspend 修饰的函数的时候,Kotlin需要追踪正在运行的协程而不是正在执行的函数
  • 绿色线条表示一个 suspend 的标记,绿色上面的是协程,绿色下面的是一个正常的函数
  • Kotlin 像正常函数一样调用 fetchDocs() 函数,在调用栈上加一个 entry,这里也存储着 fetchDocs() 函数的局部变量
  • 继续往下执行,直到找到另一个 suspend 函数的调用(这里指的是 get() 函数调用),这时候Kotlin要去实现 suspend 操作(将函数的状态从堆栈复制到一个地方,以便以后保存,所有 suspend 的协程都会被放在这里)
  • 然后调用 get() 函数,同样新建一个entry,当调用到 withContext() (withContext函数被 suspend 修饰)的时候,同样 执行suspend操作(过程和前面一样)。此时主线程里的所有协程都被 suspend,所以主线程可以做其他事情(执行 onDraw,响应用户输入)
  • 等待几秒后,网络请求会返回,这时Kotlin会执行resume操作(获取保存状态并复制回来,重新放回到调用栈上),之后会正常往下执行,如果 fetchDocs() 发成错误,会在这里抛出异常

协程的组成

val viewModelJob = Job()    //用来取消协程
    
    val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)   //初始化CoroutineScope 指定协程的运行所在线程传入 Job 方便后面取消协程

    uiScope.launch { //启动一个协程
        updateUI() //suspend函数运行在协程内或者suspend另外一个函数内
    }
复制代码
suspend fun updateUI() {
    delay(1000L) //delay是一个 suspend 函数
    textView.text = "Hello, from coroutines!"
}
复制代码
viewModelJob.cancel()//取消协程
复制代码
  • 启动一个协程需要 CoroutineScope ,为什么需要? 一会解释
  • CoroutineScope 接受 CoroutineContext 作为参数, CoroutineContext 由一组协程的配置参数组成,可以指定协程的名称,协程运行所在线程,异常处理等等。可以通过 plus 操作符来组合这些参数。上面的代码指定了协程运行在主线程中,并且提供了一个 Job ,可用于取消协程
    CoroutineName
    Job
    CoroutineDispatcher
    
  • 有了CoroutineScope之后可以通过一系列的 Coroutine builders 来启动协程,协程运行在 Coroutine builders 的代码块里面
    Job
    CoroutineContext
    

结构化并发( Structured concurrency

如果在 foo 里协程启动了 bar 协程,那么 bar 协程必须在 foo 协程之前完成

Kotlin 协程入门这一篇就够了

foo 里协程启动了 bar 协程 ,但是 bar 并没有在 foo 完成之前执行完成,所以不是结构化并发

Kotlin 协程入门这一篇就够了
foo 里协程启动了 bar 协程 ,并且 barfoo

完成之前执行完成,所以是结构化并发

结构化并发能够带来什么优势呢?下面一点点阐述。

协程的泄漏

尽管协程本身是轻量级的,但是协程做的工作一般比较重,比如读写文件或者网络请求。使用代码手动跟踪大量的协程是相当困难的,这样的代码比较容易出错,一旦对协程失去追踪,那么就会导致泄漏。这比内存泄漏更加严重,因为失去追踪的协程在 resume 的时候可能会消耗内存,CPU,磁盘,甚至会进行不再必要的网络请求。

如何避免泄漏呢?这其实就是 CoroutineScope 的作用,通过 launch 或者 async 启动一个协程需要指定 CoroutineScope ,当要取消协程的时候只需要调用 CoroutineScope.cancel() ,kotlin 会帮我们自动取消在这个作用域里面启动的协程。

结构化并发可以保证当一个作用域被取消,作用域里面的所有协程会被取消

如果使用架构组件(Architecture Components),比较适合在 ViewModel 中启动协程,并且在 onCleared 回调方法中取消协程

override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel() //取消ViewModel中启动的协程
    }
复制代码

自己写CoroutineScope比较麻烦,架构组件提供了 viewModelScope 这个扩展属性,可以替代前面的 uiScope

看下 viewModelScope 这个扩展属性是如何实现的:

val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(Job() + Dispatchers.Main))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}
复制代码

同样是初始化一个CoroutineScope,指定Dispatchers.Main和 Job

##ViewModel
    @MainThread
    final void clear() {
        mCleared = true;
        // Since clear() is final, this method is still called on mock objects
        // and in those cases, mBagOfTags is null. It'll always be empty though
        // because setTagIfAbsent and getTag are not final so we can skip
        // clearing it
        if (mBagOfTags != null) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
        onCleared();
    }

    private static void closeWithRuntimeException(Object obj) {
        if (obj instanceof Closeable) {
            try {
                ((Closeable) obj).close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
复制代码

clear() 中会自动取消作用域中的协程。有了 viewModelScope 这个扩展属性可以少些很多模板代码。

再看一个稍复杂的场景,同时发起两个或者多个网络请求。这就意味着要开启更多的协程,随处开启协程可能导致潜在的泄漏问题,调用者可能不知道新开启的协程,因此也没法追踪他们。 这时候就需要 coroutineScope 或者 supervisorScope (注意不是 CoroutineScope )。

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        launch { fetchDoc(2) }
    }
}
复制代码

这个示例中,同时发起两个网络请求。在suspend 函数里面可以通过 coroutineScopesupervisorScope 安全地启动协程。为了避免泄漏,我们希望 fetchTwoDocs 这样的函数返回的时候,在函数内部启动的协程都能执行完成。

结构化并发保证当suspend函数返回的时候,函数里面的所有工作都已经完成

Kotlin可以保证使用 coroutineScope 不会从 fetchTwoDocs 函数中发生泄漏, coroutineScopesuspend 自己直到在它里面启动的所有协程执行完成。正是因为这样, fetchTwoDocs 不会在 coroutineScope 内部启动的协程完成前返回。

如果有更多的协程呢?

suspend fun loadLots() {
        coroutineScope {
            repeat(1000) {
                launch { fetchDoc(it) }
            }
        }
    }
复制代码

这里在suspend函数中启动了更多的协程,会泄露吗?并不会。

Kotlin 协程入门这一篇就够了

由于这里的 loadLots 是一个 suspend 函数,所以 loadLots 函数会在一个 CoroutineScope 中被调用, coroutineScope 构造器会使用这个 CoroutineScope 作为父作用域生成一个新的 CoroutineScope 。在 coroutineScope 代码块内部, launch 函数会在这个新的 CoroutineScope 中启动新的协程,这个新的 CoroutineScope 会追踪这些新的协程,当所有的协程执行完毕, loadLots 函数才会返回。

coroutineScopesupervisorScope 会等到所有的子协程执行完毕。

使用 coroutineScope 或者 supervisorScope 可以安全地在 suspend 函数里面启动新的协程,不会造成泄漏,因为总是会 suspend 调用者直到所有的协程执行完毕。 coroutineScope 会新建一个子作用域(child scope),所以如果父作用域被取消,它会把取消的信息往下传递给所有新的协程。

另外 coroutineScopesupervisorScope 的区别在于: coroutineScope 会在任意一个协程发生异常后取消所有的子协程的运行,而 supervisorScope 并不会取消其他的子协程。

如何保证收到异常

前面有介绍过 async 里面如果发生异常是不会直接抛出的,直到 await 得到调用,所以下面的代码不会抛出异常。

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}
复制代码

但是 coroutineScope 会等到协程执行完毕,所以发生异常后会抛出。下面的代码会抛出异常。

suspend fun foundError() {
    coroutineScope {
        async { 
            throw StructuredConcurrencyWill("throw")
        }
    }
}
复制代码

结构化并发保证当协程出错时,协程的调用者或者他的做用户会得到通知

由此可见 结构化并发可以保证代码更加安全,避免了协程的泄漏问题

suspend

参考文档:

 Coroutines on Android (part I): Getting the background 

 Coroutines on Android (part II): Getting started 

 Understand Kotlin Coroutines on Android (Google I/O'19) 

 Using Kotlin Coroutines in your Android App 


以上所述就是小编给大家介绍的《Kotlin 协程入门这一篇就够了》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C#本质论

C#本质论

米凯利斯 / 周靖 / 人民邮电出版社 / 2010-9 / 99.00元

《C#本质论(第3版)》是一部好评如潮的语言参考书,作者用一种非常合理的方式来组织《C#本质论(第3版)》的内容,由浅人深地介绍了C#语言的各个方面。全书共包括21章及6个附录,每章开头的“思维导图”指明了本章要讨论的主题,以及各个主题之间的层次关系。书中所包含的丰富的示例代码和精要的语言比较,都有助于读者理解C#语言。《C#本质论(第3版)》首先介绍了C#语言的基础知识,随后深人讲解了泛型、迭代......一起来看看 《C#本质论》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具