Android NDK开发详解后台任务之利用 Kotlin 协程提升应用性能

本文详细介绍了如何在Android应用中使用Kotlin协程管理和优化后台任务,包括挂起函数、调度器、withContext的使用以及CoroutineScope、Job和CoroutineContext的概念。通过协程,可以确保主线程安全,提高应用性能。
摘要由CSDN通过智能技术生成

借助 Kotlin 协程,您可以编写干净、简化的异步代码,使您的应用能够及时响应,同时管理长时间运行的任务(例如网络调用或磁盘操作)。

本主题详细介绍 Android 上的协程。如果您不熟悉协程,请务必先阅读 Android 上的 Kotlin 协程,然后再阅读本主题。

管理长时间运行的任务

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

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 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。协程可以自行挂起,而调度程序负责将其恢复。

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

Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数,运行 Android 界面框架操作,以及更新 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 只切换一次线程。这样,即使网络库多次使用 withContext(),它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了 Dispatchers.Default 与 Dispatchers.IO 之间的切换,以尽可能避免线程切换。
重要提示:利用一个使用线程池的调度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保证块在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspend 和 resume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值。

启动协程

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

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

通常,您应使用 launch 从常规函数启动新协程,因为常规函数无法调用 await。只有在另一个协程内或在挂起函数内且在执行并行分解时,才使用 async。
警告:launch 和 async 处理异常的方式不同。由于 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
    }

虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 awaitAll(),coroutineScope 构建器也会等到所有新协程都完成后才恢复名为 fetchTwoDocs 的协程。

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

如需详细了解并行分解,请参阅编写挂起函数。

协程概念

CoroutineScope

CoroutineScope 会跟踪它使用 launch 或 async 创建的任何协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope。不过,与调度程序不同,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.cancel()。使用 viewModelScope 时,ViewModel 类会在 ViewModel 的 onCleared() 方法中自动为您取消作用域。

作业

Job 是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 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:控制协程的生命周期。
CoroutineDispatcher:将工作分派到适当的线程。
CoroutineName:协程的名称,可用于调试。
CoroutineExceptionHandler:处理未捕获的异常。

对于在作用域内创建的新协程,系统会为新协程分配一个新的 Job 实例,而从包含作用域继承其他 CoroutineContext 元素。可以通过向 launch 或 async 函数传递新的 CoroutineContext 替换继承的元素。请注意,将 Job 传递给 launch 或 async 不会产生任何效果,因为系统始终会向新协程分配 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,请参阅协程博文中的异常

其他协程资源

如需获取更多协程资源,请访问以下链接:

Android 上的 Kotlin 协程
有关 Kotlin 协程和流程的其他资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值