Android协程简介 -- 基础概念

前言

之前通过一个登录请求,简单了解Android协程的实现方式。相信很多人和我刚开始的时候一样,看的云里雾里的不清楚。这是因为我们对协程缺乏基础的了解。这篇文章,我们就详细介绍一下协程相关的基础知识。

文章分为以下几个部分:

  1. 协程的概念
  2. 协程优点
  3. 协程相关的类、函数
  4. 结构化并发

协程的概念

本质上,协程是轻量级的线程。协程是运行在固定的协程程序范围(CoroutineScope及其衍生子类)上下文中,与协程构建器(launch、async)一起启动。协程有自己的生命周期,其生命周期受到启动其协程程序范围(CoroutineScope及其衍生子类)的上下文对应的生命周期限制。我们来看下面的代码:

fun coroutineTest(){
  //viewModelScope Viewmodel类中CoroutineScope类的衍生子类
  //launch
  viewModelScope.launch(Dispatchers.IO) { 
    delay(2000L)
    //在viewModel中创建了一个新的协程使其运行在io线程并打印一行日志信息
    Log.i("viewModelScope" , "New coroutine!")
  }
}

在上面代码中我们在ViewModel中创建了一个新的协程,使其运行在io线程,并在延时2秒之后打印了一行日志。那么该协程的生命周期就收到整个ViewModel的生命周期所限制,当ViewModel被销毁或者回收时,即使该协程不再主线程中,也会被同步取消并回收。

这里需要注意,在使用协程时要注意协程启动的作用域,避免页面销毁后,仍需要工作的协程被同步销毁。

协程的优点

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。

    fun coroutineLightweight(){
        val start = System.currentTimeMillis()
        //启动100个协程,每个协程打印一行日志
        repeat(100){
            viewModelScope.launch {
                Log.i("viewModelScope" , "coroutine num is $it")
                if (it == 100){
                    Log.i("viewModelScope" , "use time is ${System.currentTimeMillis() - start}")
                }
            }
        }
    }
    
    

    打印结果:

    coroutine num is 99
    use time is 41
    
    

    我们创建了100个协程并打印日志,总耗时只有41毫秒,但是如果我们用线程的话,如果启动数再大一些,估计直接就会出现内存不足的情况了。

  • 内存泄漏更少:具有完整的生命周期管理,宿主上下文被销毁后会被同步销毁,能够有效的避免内存泄漏的问题。使用结构化并发机制在一个作用域内执行多项操作。

  • 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。

  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域(ViewModel,Lifecycled…),可供您用于结构化并发。

协程相关的类、函数

  • CoroutineScope:协程作用域。
  • launch、async:协程启动器
  • Dispatchers:调度程序,用来分配对应的协程工作对应的线程
  • job:协程的句柄,启动协程后生成的对象,便于异步情况下对以启动的协程执行操作(取消、暂停、释放等)
  • CoroutineContext:协程上下文

介绍以上内容之前我们先看一段代码:

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)
        /* 在这里执行网络IO操作 */             // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}


在上面代码中,我们声明了两个挂起的函数,第一个运行在主线程上,第二个耗时函数运行在io线程中。

CoroutineScope

CoroutineScope是协程的作用域,每个协程必须在对应的作用域中启动。作用域一般包含完整的生命周期。每个协程的作用域都会跟踪在作用域内通过launch或者async创建的任何协程,并且可以通过scope.cancel()函数来取消正在运行中的协程。Android的KTX库中,某些生命周期类提供了自己的CoroutineScope。例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope。不过,与调度程序不同,CoroutineScope 不运行协程。

viewModelScope创建并启动协程,可以参考Android协程的实现。当然,如果你需要创建自己的协程作用域(CoroutineScope)以控制协程在应用的特定层中的生命周期,则可以创建一个如下所示的 CoroutineScope:

class ExampleClass {

    //Job和Dispatcher被组合成CoroutineContext,构建一个新的协程作用域对象
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // 在作用域内启动一个新的协程
        scope.launch {
            // 在新的协程内执行一些挂起的函数
            fetchDocs()
        }
    }

    fun cleanUp() {
        //取消作用域以取消正在该作用域中运行的线程
        scope.cancel()
    }
}

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

launch、async

launch 和 aync都是协程的启动器,其中

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

通常,我们使用 launch 从常规函数启动新协程,因为常规函数无法调用 await。只有在另一个协程内或在挂起函数内需要同时并发执行多个工作任务时,才使用 async。另外值得注意的是,由于 async 希望在某一时刻对 await 进行最终调用,因此它持有异常并将其作为 await 调用的一部分重新抛出。这意味着,如果您使用 async 从常规函数启动新协程,则会以静默方式丢弃异常。这些丢弃的异常不会出现在崩溃指标中,也不会在 logcat 中注明。这样会是的一单在async中出现问题,问题原因会变得难以定位。

async的用法可见下文中结构化并发部分

Dispatchers

Dispatchers是协程的调度程序,它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

如果,我们要在主线程之外运行代码,可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。协程可以自行挂起,而调度程序负责将其恢复。

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

  • Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数,运行 Android 界面框架操作,以及更新 LiveData 对象。
  • Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
  • Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。

此外,我们还可以使用newSingleThreadContext(“thread name”)函数,再启动协程中的同时,创建一个新的依赖线程进行工作。需要主意的是,一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数释放线程,或存储在一个顶层变量中使它在整个应用程序中可以被重用。

我们可以使用withContext(/调度程序类型/)或者launch(/调度程序类型/)函数来声明协程的依赖线程。

launch { // 默认情况,运行在父协程的上下文中。如果没有父协程,则运行在主线程中
    println("main                  : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) { //将会运行在IO线程中
    println("IO                    : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // 将会获取默认调度器
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 将使它获得一个新的专属线程
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
} 

withContext()函数使用类似去lanuch。

job

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

class ExampleClass {
    fun exampleMethod() {
        // 声明协程对象,可以通过该对象控制协程声明周期
        val job = scope.launch {
            // 新协程
        }

        if (/*取消条件*/...) {
            //取消上面启动的协程,但是并不会对启动协程的作用域产生影响
            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() {
        //在主线程上默认作用域上启动一个新的协程
        val job1 = scope.launch {
            // 新协程的名称 = "coroutine" (默认)
        }

        // 在默认线程上启动一个新的协程,并重新定义新的协程名称
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // 新协程的名称 = "BackgroundCoroutine" (重写替换)
        }
    }
}

结构化并发

在kotlin中,每个协程都有自己作用域,并且协程具备一下特点

  • 在父协程作用域内新建的字写成作用域都属于它的子作用域;
  • 父协程和子协程存在级联关系;
  • 父协程需要等待子协程全部执行完成后才会结束;
  • 主动取消父协程作用域时,在该作用域内创建的所有子协程作用域都被结束。

在 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() =        
    coroutineScope {
        val deferreds = listOf(     // 同时获取两份文档
            async { fetchDoc(1) },  // 同步返回第一个文档的结果
            async { fetchDoc(2) }   // 同步返回第二个文档的结果
        )
        deferreds.awaitAll()        // 使用awaitAll函数等待两个网络请求
    }

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

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值