利用Kotlin协程提升应用性能

借助Kotlin协程,可以编写整洁,简化的异步代码,用来管理长时间运行的任务(例如网络调用或磁盘操作),使应用保持快速响应。同时管理长时间运行的任务(例如网络调用或磁盘操作)。

本文详细介绍协程的高级概念。如果不熟悉协程,可先阅读Android上的协程:简介,然后再阅读本文。

管理耗时任务

协程在常规函数的基础上添加了两项操作,用于处理耗时任务。在invoke(或call)和return之外,协程添加了suspendresume

  • suspend用于暂停执行当前协程,并保存所有局部变量。
  • resume用于让已挂起的协程从挂起处继续执行。

如需调用suspend函数,只能从他suspend函数进行调用,或通过使用协程构建器(例如launch)来启动新协程。如下:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

在上面示例中,get()仍在主线程上运行,但他会在启动网络请求之前挂起协程。当网络请求完成时,get会恢复已挂起的协程,而不是使用回调通知主线程。

Kotlin使用堆栈管理要运行哪个函数以及所有局部变量。挂起协程时,系统会复制并保存当前的堆栈帧以供稍后使用。恢复时,会将堆栈帧从其保存位置复制回来,然后函数再次开始运行。使代码可能 看起来像普通的顺序阻塞请求,协程也能确保网络请求避免阻塞主线程。

使用协程确保主线程安全

Kotlin协程使用调度器确定哪些线程用于执行协程。要在主线程之外运行代码,可以让Kotlin协程在DefaultIO调度器上执行工作。在Kotlin中,所有协程都必须在调度器中运行,即使它们在主线程上运行也是如此。协程可以自行挂起,而调度器负责将其恢复。

Kotlin提供了三个调度器,用于指定应在何处运行协程:

  • Dispatchers.Main - Android主线程上运行协程,些调度器只能用于与UI交互和执行不耗时的工作。包括调用suspend函数,Android UI框架操作,以及更新LiveData对象。
  • Dispatchers.IO - 经过优化的调度器,适合在主线程之外执行磁盘或网络I/O。如:Room组件、文件读/写数据,以及运行任何网络操作。
  • Dispatchers.Default - 经过优化的调度器,适合在主线程之外执行占用大量CPU资源的工作。如:列表排序和解析JSON

接着前面示例来讲,还可以使用高度程序重新定义get函数。在get的函数内,调用withContext(Dispatchers.IO)来创建一个在IO线程中运行的代码块。放在该代码块内的任何代码都始终通过IO调度程序执行。由于withContext本身就是一个挂起函数,因此get也是一个挂起函数。

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

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

借助协程可以通过精细控制来调度线程。由于withContext可以在不引入回调的情况下在线程池中执行任何一行或几行代码,因此可以将其应用于非常小的函数,例如从数据库中读取数据或执行网络请求。一种不错的做法是使用withContext()来确保每个函数都是主线程安全的。这意味着,可以从主线程调用任意函数。这样,调用方就不需要考虑具体应用使用哪个线程来执行函数了。

在前面的示例中,fetchDocs()虽在主线程上执行;它也可以安全地调用有get,还是会在后台执行网络请求。由于协程支持suspend和resume,因此withContext代码块完成后,主线程上的协程会立即根据get的结果恢复至原来的线程。

重要提示:使用 suspend 不会让 Kotlin 在后台线程上运行函数。suspend 函数在主线程上运行是一种正常的现象。在主线程上启动协程的情况也很常见。当您需要确保主线程安全时(例如,从磁盘上读取数据或向磁盘中写入数据、执行网络操作或运行占用大量 CPU 资源的操作时),应始终在 suspend 函数内使用 withContext()

withContext()的作用

与基于回调的等效实现相比,withContext()不会增加额外的开销。此外,在某些情况下,还可以优化withContext()调用,使其超越基于加调的等效实现。例如,如果某个函数对一个网络进行十次调用,可以使用外部withContext()让Kotlin只切换一次线程。此外,Kotlin还优化了Dispatchers.DefaultDispathcers.IO之间的切换,以尽可能避免线程切换。

重要提示:利用一个使用线程池的调度程序(例如 Dispatchers.IODispatchers.Default)不能保证块在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspendresume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值。

启动协程

可以通过以下两种方式来启动协程:

  • launch可启动新协程而不将结果返回给调用方。任何被视为“一劳永逸”的工作都可以使用launch来启动。
  • async可启动新协程并允许使用名为await的挂起函数返回结果。

通常,应使用launch来启动常规的新协程,只有在另一个协程内或在挂起函数内且在执行并行分解时才使用async

警告launchasync 处理异常的方式不同。由于 async 希望在某一时刻对 await 进行最终调用,因此它持有异常并将其作为 await 调用的一部分重新抛出。这意味着,如果您使用 async 从常规函数启动新协程,则能以静默方式丢弃异常。这些丢弃的异常不会出现在崩溃指标中,也不会在 logcat 中注明。如需了解详情,请参阅协程中的取消和异常

并行分解

suspend函数启动的所有协程都必须在该函数返回结果时停止,因此需要保证这些协程在返回结果前完成。借助Kotlin中的结构化并发机制,可以定义用于启动一个或多个协程的coroutineScope。然后,可以使用await()(针对单个协程)或awaitAll()(针对多个协程)保证这些协程在从函数返回结果前完成。

例如,假设一个用于异步获取两个文档的coroutineScope。通过对每个延迟引用调用await(),可以保证这两项async操作在返回之前完成

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

还可以对集合使用awaitAll(),代码如下所示:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

值得注意的是,即使没有调用awaitAll()coroutineScope构建器也会等到所有 新协程都完成后才恢复名为fetchTwoDocs的协程。

此外,coroutineScope会捕获协程抛出的所有异常,并将其传送加调用方。

如需详细了解并行分解,可以阅读编写挂起函数

协程概念

CoroutineScope

CorutineScope会跟踪使用launchasync 创建的任何协程。可以随时调用 scope.cancel()来取消正在运行的协程。在Android中,某些KTX库为某些生命期提供自己的 CoroutineScope。 例如,ViewModelviewModelScopeLifecyclelifecylceScope。不过,与高度程序不同,CoroutineScope不运行协程。

注意:如需详细了解 viewModelScope,请参阅 Android 中的简易协程:viewModelScope

viewModelScope 也可用于 Android 上采用协程的后台线程中的示例内。但是,如果您需要创建自己的 CoroutineScope 以控制协程在应用的特定层中的生命周期,则可以创建一个如下所示的 CoroutineScope:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

已取消的scope无法再创建协程。因此,公当控制其生命周期的类被销毁时,才应调用 scope.cancel()。使用 viewModelScope时,ViewModel 类会在 ViewModelonCleared() 方法中自动取消 scope

JOB

Job 是协程的句柄。使用 launchasync 创建的每个协程都会返回一个 Job 实例,该实例是相应协程的唯一标识并管理其生命周期。还可以将 Job 传递给 CoroutineScope 以进一步管理其生命周期,如以下示例所示:

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext 使用以下元素集定义协程的行为:

对于在作用域内创建的新协程,系统会为新协程分配一个新的 Job 实例,而从包含作用域继承其他 CoroutineContext 元素。可以通过向 launchasync 函数传递新的 CoroutineContext 替换继承的元素。请注意,将 Job 传递给 launchasync 不会产生任何效果,因为系统始终会向新协程分配 Job 的新实例。

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

注意 :如需详细了解 CoroutineExceptionHandler,请参阅协程博文中的异常

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容