Android神兵利器之协程和Lifecycle

2023-12-16

导语

一个安卓开发究竟要经历怎样的颠沛流离才终于能遇见Jetpack,遇见协程和Lifecycle。

在Jetpack出现以前安卓应用架构这一块可以说非常混乱,早期没有官方架构组件,小公司可能都是mvc一把梭或者引入了简易版的mvp模式,而大厂可能就是更加庞杂的自建组件的融合,而且由于历史包袱各个模块代码在架构演进过程中发展也不同,换一个模块就会换一种完全不同的代码组织方式也不罕见。

好在近几年随着kotlin在安卓扎根,加上google对于jetpack的大力建设,可以预见的很长时间的未来安卓应用架构都会以jetpack提供的架构组件为基底,演进为大同小异的mvvm模式。

今天我就来说一说其中比较重要的两个部分,协程和Lifecycle,本文会把重心放在使用它们的一些最佳实践和容易遇到的问题上面。

开刃

协程毫无疑问是一把所向披靡的利刃,可以极大简化我们的异步代码,但是我发现很多开发者对于它的使用还是太局限了,就像挥舞着一把没有开刃的刀子

也许你常用的方式是像这样,在viewModel中做网络请求

viewModelScope.launch {
    val data = withContext(Dispatchers.IO) {
        ......
    }
    liveData.value = data
}

但是除了网络之外呢,当然还有很多其他的应用场景,我将由浅到深介绍几个例子

延时任务

通常情况下我们会像这样启动一个延时任务

private val handler = Handler(Looper.getMainLooper())
private val task = Runnable {}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    handler.postDelayed(task, 1000)
}

override fun onDestroyView() {
    super.onDestroyView()
    handler.removeCallbacks(task)
}

麻烦,而且有坑,还要手动管理生命周期

而使用协程就非常方便

lifecycleScope.launch { 
    delay(1000)
    dotask...
}

不再需要管理生命周期,这个延时任务会在Lifecycle.State.DESTROYED时取消

也可以指定启动的生命周期,这样它就会在与之相反的生命周期自动取消

lifecycleScope.launch {
    // stop时取消
    lifecycle.withStarted { 
        delay(1000)
        dotask...
    }
}

周期性任务

倒计时是一个常见的周期性任务,通常也需要使用handler来实现,用协程当然能更加方便

val totalTime = 10000
lifecycleScope.launch {
    while (isActive && totalTime > 0) {
        delay(1000)
        totalTime -= 1000
        tickTask...
    }
}

如此简单,但是还可以更强大

很多情况下倒计时也需要自动暂停和恢复功能,刚好有一个api可以帮我们做到,来改造一下

val totalTime = 10000
lifecycleScope.launch {
    owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        while (isActive) {
            delay(1000)
            totalTime -= 1000
            tickTask...
        }
    }
}

好了,现在这个倒计时可以在每次onStop时暂停,onStart时恢复了,非常完美

repeatOnLifecycle会在到达指定的生命周期时启动协程,在相反的生命周期中取消,关于repeatOnLifecycle的实现不在本文涉及范围内,有兴趣可以自行查阅

这里有一个注意点就是不要在可能进行多次的生命周期回调(onStart onResume)中使用repeatOnLifecycle

生产消费者模型

生产消费者模型也是比较常用的场景,比如自动滚动的用户中奖信息,或者直播间底部单行入场消息这种需要定时轮询队列刷新ui的场景。传统的做法是使用阻塞/非阻塞队列作为管道,然后启动一个线程或者使用handler定时不断轮询队列,缺点在于使用起来较为麻烦,而且用阻塞队列还需要至少一个阻塞线程的开销。

这里我以一个直播间单行滚动入场消息组件为例子看看使用协程要怎么使用生产消费者模型

入场消息组件的效果看起来是一个单行的自动向上滚动的recyclerview,但是实际上由于用户可见的view总是只有上一个和下一个,所以用viewflipper就可以实现

<ViewFlipper
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/flipper"
    android:flipInterval="0"
    android:orientation="vertical"
    android:inAnimation="@anim/live__view_flipper_anim_in"
    android:outAnimation="@anim/live__view_flipper_anim_out"
    >
    <TextView
        android:id="@+id/message_current"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
    <TextView
        android:id="@+id/message_next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
    />
</ViewFlipper>

接下来我们需要 使用到协程的channel组件代替队列

简单介绍一下channel

Channel

channel顾名思义是一个管道,在许多其他语言和框架中都有类似概念,发送端和接受过一个单流向的管道连接,支持多种不同的策略

通常通过一个伪装成构造函数的Channel方法来创建channel,通过分析这个方法了解一下channel的各个策略

public fun <E> Channel(
    // 容量,相当于队列容量
    capacity: Int = RENDEZVOUS,
    // 队列溢出(超出给定容量时)的处理策略
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    // 溢出时send失败数据的处理回调
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
    when (capacity) {
        RENDEZVOUS -> {
            if (onBufferOverflow == BufferOverflow.SUSPEND)
                // 不存储任何元素的,send以后如果没有receive,send协程会一直挂起
                BufferedChannel(RENDEZVOUS, onUndeliveredElement) // an efficient implementation of rendezvous channel
            else
                // 只保存一个元素,支持DROP_OLDEST和DROP_LATEST两种策略
                ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel
        }
        CONFLATED -> {
            require(onBufferOverflow == BufferOverflow.SUSPEND) {
                "CONFLATED capacity cannot be used with non-default onBufferOverflow"
            }
            // 只保存一个元素,默认DROP_OLDEST策略
            ConflatedBufferedChannel(1, BufferOverflow.DROP_OLDEST, onUndeliveredElement)
        }
        // 无限容量
        UNLIMITED -> BufferedChannel(UNLIMITED, onUndeliveredElement) // ignores onBufferOverflow: it has buffer, but it never overflows
        BUFFERED -> { // uses default capacity with SUSPEND
            // 默认容量,溢出时send挂起
            if (onBufferOverflow == BufferOverflow.SUSPEND) BufferedChannel(CHANNEL_DEFAULT_CAPACITY, onUndeliveredElement)
            // 只保存一个元素,支持DROP_OLDEST和DROP_LATEST两种策略
            else ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)
        }
        else -> {
            // 指定容量,溢出时send挂起
            if (onBufferOverflow === BufferOverflow.SUSPEND) BufferedChannel(capacity, onUndeliveredElement)
            // 指定容量,溢出时支持DROP_OLDEST和DROP_LATEST两种策略
            else ConflatedBufferedChannel(capacity, onBufferOverflow, onUndeliveredElement)
        }
    }

channel总共有三种策略,对应onBufferOverflow参数

  • SUSPEND:当channel中没有任何元素时,调用receive方法的协程将被挂起,当channel中元素超过容量时,调用send方法的协程将被挂起
  • DROP_OLDEST:当channel中元素超过容量时,调用send方法不会挂起,会把最早的那个元素移除
  • DROP_LATEST:当channel中元素超过容量时,调用send方法将会把最近的那个元素移除

capacity参数支持四种类型的值,这个参数实际上就是一种语义化参数,对上层隐藏channel的不同实现,我觉得这一块的代码反而有些画蛇添足了

总之channel在这里实际上只有两个实现,溢出挂起的BufferedChannel(可以指定容量)和溢出丢弃的ConflatedBufferedChannel(可以指定容量和策略,但是策略不能是BufferOverflow.SUSPEND)

注意我们一直强调的是挂起而非阻塞,这就是channel比起阻塞队列更有优势的地方,协程挂起并不会阻塞线程

接下来继续实现我们的组件

class ArriveMessageFlipper @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
    val viewBinding: LiveDanmuArriveFlipperBinding
    private var current: TextView
    private var next: TextView
    private val channel = Channel<String>(100, BufferOverflow.DROP_OLDEST)

    init {
        val view = inflate(context, R.layout.live__danmu_arrive_flipper, this)
        viewBinding = LiveDanmuArriveFlipperBinding.bind(view)
        current = viewBinding.messageCurrent
        next = viewBinding.messageNext
    }

    fun start(owner: LifecycleOwner) {
        owner.lifecycleScope.launch {
            // 自动唤醒和挂起,onStart唤醒,onStop挂起
            owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                while (isActive) {
                    // 队列无消息挂起
                    val item = channel.receive()
                    next.text = item
                    viewBinding.flipper.showNext()
                    // 交换两个view
                    current = next.also {
                        next = current
                    }
                    delay(500)
                }
            }
        }
    }

    suspend fun addData(item: String) {
        channel.send(item)
    }
}

  1. 创建一个容量为100自动丢弃最早元素的channel
  2. 启动时repeatOnLifecycle开启一个自动暂停恢复的轮询任务,每500毫秒取下一个消息滚动展示,无消息时协程挂起

代码就这么多,但是实际上仍然有优化的空间,让我放到下一节再讲

通过三个比较常用的实例我们发掘了协程更多的使用场景,但远非全部,这里只是作为引子希望能对各位有所启发。

不妨自行想想协程中的select又能在哪些场景使用?

收鞘

如果说协程是一把利刃那么Lifecycle无疑是可靠的剑鞘,缺了Lifecycle约束的协程难免会伤到自己

在有Lifecycle之前,开发中对于生命周期的管控真的比较麻烦,要非常小心不能漏掉任何异步任务的手动取消代码,否则就可能造成内存泄漏,至今让我记忆犹新的就是当初使用rxjava时的自动生命周期管理库 RxLifecycle ,而现在一切都变得简单而灵活

常见场景

在上一节开刃中我们已经自然而然地在使用Lifecycle,这就是它最常用的使用场景,activity和fragment都属于LifecycleOwner,所以我们可以直接使用lifecycleScope启动协程,而viewModel中也有对应的viewModelScope,通常情况下我们只管启动,而不用关注取消

值得一提的是 viewModelScope的生命周期往往比activity/fragment的lifecycleScope的要长 ,所以 不应该在fragment/activity中直接使用viewModelScope ,就像下面这样

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    xxx.viewModelScope.launch {
        ......
    }
}

另外,也应该 避免使用GlobalScope ,使用协程的大多数场景都应该使用lifecycleScope,除非你的使用场景需要跨Activity生命周期

Fragment的Lifecycle

Fragment拥有两个Lifecycle, lifecycle viewLifecycleOwner.lifecycle 前者是fragment本身的,后者是fragment中view的,他们的生命周期不总是一致的,比如将fragment加入backstack或者使用FragmentPagerAdapter,fragment实例就会被保存下来,而view会被销毁。 所以在fragment中最好只使用viewLifecycleOwner,同时要记住viewLifecycleOwner在onCreateView之后才被创建。

在View中感知Lifecycle

除了Activity/Fragment这种本身就是LifecycleOwner的组件,也可以通过添加观察者的方式为所有实例添加生命周期感知能力,以上面的ArriveMessageFlipper为例,来稍作改造

  • ArriveMessageFlipper实现DefaultLifecycleObserver
class ArriveMessageFlipper @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs), DefaultLifecycleObserver {

    init {
        ......
    }
    
    override fun onCreate(owner: LifecycleOwner) {
        lifecycleOwner = owner
        start(owner)
    }

    fun start(owner: LifecycleOwner) {
        ......
    }

    fun addData(item: LiveItemCommonMessageModel) {
        lifecycleOwner?.lifecycleScope?.launch {
            channel.send(item)
        }
    }
    
    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        lifecycleOwner = null
    }
}

在fragment中将它注册为观察者

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewLifecycleOwner.lifecycle.addObserver(viewBinding.arriveFlipper)
}

这样就可以让flipper在onViewCreated时自动启动了

但是还没完,如果是在view中想要关注生命周期,实际上我们可以连注册观察者这一步都省掉,代码看起来像下面这样

class ArriveMessageFlipper @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
    ...
    
    init {
        ...
        doOnAttach {
            val owner = findViewTreeLifecycleOwner()
            owner?.lifecycleScope?.launch {
                // 自动唤醒和挂起,onStart唤醒,onStop挂起
                owner.lifecycle?.repeatOnLifecycle(Lifecycle.State.STARTED) {
                    while (isActive) {
                        // 队列无消息挂起
                        val item = channel.receive()
                        next.text = item
                        viewBinding.flipper.showNext()
                        // 交换两个view
                        current = next.also {
                            next = current
                        }
                        delay(500)
                    }
                }
            }
        }
    }

    fun addData(item: LiveItemCommonMessageModel) {
        findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
            channel.send(item)
        }
    }
}

view不用再继承DefaultLifecycleObserver,而是在首次onAttach时通过 findViewTreeLifecycleOwner 找到当前所属的lifecycleOwner(fragment view/activity),使用它来自动启动任务

通过这种方式就可以完全将View和Activity/Fragment解藕,将页面拆分为一个个View,在View的内部处理自己的业务逻辑,同时还能够感知生命周期,能够很好地复用View,对于复杂的页面无疑也有非常大的帮助

总结

本文并没有讲什么比较深入的东西,主要还是从实际应用出发让我们对协程和Lifecycle有更多的理解,能更加灵活地使用它们提升代码质量和开发效率。在架构演进这一过程中最核心的问题还是解决实际问题,而不是为了演进而演进,以旧架构的思维来使用新架构新工具,否则终究还是屎山堆积。

最后

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

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

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓ (文末还有ChatGPT机器人小福利哦,大家千万不要错过)

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

图片

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android神兵利器之协程和Lifecycle 的相关文章

随机推荐

  • Linux环境变量执行顺序

    环境变量执行顺序 etc profile etc profile d sh bash profile bashrc etc bashrc
  • 生意参谋竞品分析RPA机器人,让你在商战中立于不败之地

    作为电商企业 了解竞争对手的动态和策略对于制定有效的竞争策略至关重要 但是竞对分析是一项繁琐而费时的工作 往往需要大量的人力和时间投入 在这样的情况下 八爪鱼rpa机器人的出现为电商企业带来了新的解决方案 rpa机器人是一种基于自动化软件的
  • mysql执行带函数命令的sql脚本报错

    一 前言 开发给了一个带函数的sql文件让我执行 但是执行导入时报以下错误 This function has none of DETERMINISTIC NO SQL or READS SQL DATA in its declaratio
  • 万字整理Redis核心知识点

    1 Redis介绍 Redis 是 NoSQL 但是可处理 1 秒 10w 的并发 数据都在内存中 使用 java 对 redis 进行操作类似 jdbc 接口标准对 mysql 有各类实现他的实现类 我们常用的是 druid 其中对 re
  • mysql开启查询日志

    mysql默认不开启查询日志 可以通过命令查询 show VARIABLES LIKE general 开启查询日志 并更改日志存放目录 不过存放的目录一定要有权限不然会报错 手动创建一下log目录下的mysql目录并赋予权限 mkdir
  • 客户案例 | 博睿数据全面保障昆仑银行业务稳定性

    新兴市场和不断增长的客户群体需求的崛起 正推动着基于互联网模式的财富陪伴 财富管理和财富生态的全新业务范式的形成 昆仑银行是一家总部位于北京 分支机构遍布全国性的城商行 提供广泛的金融产品和服务 主要包括个人银行业务 企业金融服务 资产管理
  • final的安全发布

    final的安全发布 两个关键字 发布 安全 所谓发布通俗一点的理解就是创建一个对象 使这个对象能被当前范围之外的代码所使用 比如Object o new Object 然后接下来使用对象o 但是对于普通变量的创建 之前分析过 大致分为三个
  • Postgresql在Windows中使用pg_dump实现数据库(指定表)的导出与导入

    场景 Windows中通过bat定时执行命令和mysqldump实现数据库备份 Windows中通过bat定时执行命令和mysqldump实现数据库备份 mysqldump bat CSDN博客 Windows上通过bat实现不同数据库之间
  • 程序员那么卷,就业那么难,为什么你还当一名程序员

    前言 这是很早之前看到的一个问题 那时候应该也和今年的情形一样 只不过没有现在这么严重 因为以前只是企业一方面的问题导致的裁员潮流 而到了2023年就不仅仅是因为疫情之类的原因导致企业不景气的问题 更多的是程序员太多了 是的相比较与10年轻
  • 用RPA解放人力,实现未发货订单超时预警

    在电商行业中 未发货订单的处理是一个重要的环节 对于电商企业而言 及时发货是保证客户满意度的关键 然而 由于订单数量庞大 人工处理订单需要耗费大量时间和人力资源 容易出现遗漏和延误的情况 影响客户体验和企业形象 在面对未发货订单超时预警这一
  • SpringBoot+线程池实现高频调用http接口并多线程解析json数据

    场景 Springboot FastJson实现解析第三方http接口json数据为实体类 时间格式化转换 字段包含中文 Springboot FastJson实现解析第三方http接口json数据为实体类 时间格式化转换 字段包含中文 C
  • 学籍服务平台省内转学批量自动申请

    学籍服务平台是指用于管理学生学籍信息的在线平台 包括学生的基本信息 学习成绩 奖惩记录等 在学籍服务平台上 学生可以进行选课 申请转学等操作 然而 目前的学籍服务平台存在一些问题 繁琐的操作流程 目前的学籍服务平台上 学生申请转学需要填写大
  • 用RPA轻松实现课程自动通知

    在教育领域 课程通知是一项重要的工作 但通常需要教师手动发送通知 记录学生反馈等繁琐的操作 这不仅耗费教师大量时间和精力 还容易出现遗漏或错误 为了提高效率和减轻教师的工作负担 可以使用八爪鱼rpa实现课程自动通知 八爪鱼rpa是一款强大的
  • mitm抓包实践---可用于投票、日常类任务运用

    文章目录 一 安装mitm 二 证书导入 三 抓包 三 后话补充 一 安装mitm 第一种方式 官网下载 https mitmproxy org downloads 第二种方式 py库安装 pip install mitmproxy 我是第
  • SVM原理理解

    目录 概念推导 共识 距离两个点集距离最大的分类直线的泛化能力更好 更能适应复杂数据 怎么能让margin最大 最大化margin公式 求解最大margin值 拉格朗日乘子法 为什么公式中出现求和符号 SVM模型 求解拉格朗日乘子 如何求解
  • 技术面试,如何谈薪资?

    众所周知 程序员是一个很容易出现薪资倒挂的职业 工作 3年比工作 5年薪资高的例子比比皆是 在 你手上有 offer吗 文章中 我们分析了如何巧妙地谈 offer 今天我们一起来分析如何谈薪资 顺利实现薪资倒挂 守住底线 不管是主动换工作还
  • 留给兼容安卓时间不多了!华为原生鸿蒙系统越来越近:跟iOS、安卓一样独立

    前言 据国内媒体报道称 余承东已经明确表态 华为明年将会推出鸿蒙原生应用与原生体验 HarmonyOS NEXT的产品 现在的情况就是 鸿蒙留给兼容安卓生态的时间越来越少了 而在之前已经有不少App厂商转入到他们的生态 并已经在开发相关的A
  • Docker仓库加密认证

    一 强制使用非加密访问仓库 insecure registry 实验环境 准备第二台虚拟机并配置docker服务及开启等 并把文件拷贝到第二台 记得配置好两台虚拟机仓库名的解析 配置步骤 1 配置文件使用非加密端口 vim etc dock
  • 鸿蒙开发入门:快速修复

    快速修复概述 快速修复是HarmonyOS系统提供给开发者的一种技术手段 支持开发者以远快于应用升级的方式对应用程序包进行缺陷修复 和全量应用升级软件版本相比 快速修复的主要优势在小 快和用户体验好 在较短的时间内不中断正在运行的应用的情况
  • Android神兵利器之协程和Lifecycle

    导语 一个安卓开发究竟要经历怎样的颠沛流离才终于能遇见Jetpack 遇见协程和Lifecycle 在Jetpack出现以前安卓应用架构这一块可以说非常混乱 早期没有官方架构组件 小公司可能都是mvc一把梭或者引入了简易版的mvp模式 而大