Android神兵利器之协程和Lifecycle

导语

一个安卓开发究竟要经历怎样的颠沛流离才终于能遇见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,lifecycleviewLifecycleOwner.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机器人,可以解答大家在工作上或者是技术上的问题

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值