深入探索Jetpack Compose LazyColumn性能表现及优化策略

自从Jetpack Compose作为Google推出的现代化UI工具包以来,它在Android开发领域掀起了不小的浪潮。Compose的声明式、响应式让UI构建变得前所未有的简洁高效。

然而,在大量数据场景下,Compose内置列表组件LazyColumn的性能却一直受到质疑和探讨。本博客将深入分析LazyColumn的工作原理,揭示它遇到的性能瓶颈,并介绍一些优化手段,最后与经典的RecyclerView进行性能对比。

LazyColumn工作原理:LazyColumn是Compose中用于呈现大量重复数据的列表组件,其设计理念借鉴了Bruce法则:数据驱动 虚拟化、离屏回收等技术。具体来说,LazyColumn会根据用户可视区域,只渲染极少部分实际可见的内容。当滚动时,Compose会智能回收离开屏幕的已渲染组合项,并重新组合新出现的项目,从而大幅节省内存和CPU开销。

然而,这种基于状态跟踪的响应式重组机制,在处理大规模列表时仍可能遇到一些瓶颈和性能问题。

性能问题分析,通过Traceview等诊断工具,我们可以详细分析LazyColumn在大数据场景下的性能表现。主要发现了以下几个方面的问题:

  1. 重组开销大,当列表数据发生变更时,LazyColumn需要重新组合受影响的项目。大规模列表意味着更多项目需要重组,从而带来较大的CPU和内存开销。

  2. 内存抖动问题,由于Kotlin和Compose都是基于JVM的,因此也会遇到经典的GC和内存抖动问题。大数据列表增加了这方面的风险。

    内存抖动(Memory Churn)指的是在运行应用程序时,内存的使用情况存在剧烈的波动,出现内存的急剧分配和释放的情况。这种

  3. 绘制效率低,由于Compose需要在低层次API上反复执行测量和布局操作,这会影响最终渲染速度。RecyclerViewViewHolder模型在此有一定优势。

  4. 项目复杂度影响LazyColumn可复用性有限,对于需要复杂状态跟踪的列表项,组合项本身就可能成为性能瓶颈。

针对这些问题,Compose和社区提出了一些优化方案和建议。

一、优化方案

  1. 使用Stable声明,利用kotlinx.collections.immutable包提供的ListMap等稳定集合,配合Compose 1.2引入的@Stable注解,可以避免意外的重组发生。
@Composable
fun MyItem(@Stable items: List<Item>) {
    // ...
}

  1. 状态提升,复杂列表项内部状态跟踪也可能带来性能问题。因此建议将状态提升到更高级别进行集中管理,以避免底层地的重组。

  2. 进阶的虚拟化技术Compose 1.3引入的LaxyLayoutPolicy API,可以为LazyColumnLazyRow等定制精细化的虚拟化策略,充分发挥虚拟化优势。

fun pageVirtualizationPolicy(pageSize: Int) = LazyLayoutPolicy {
    it.policyFor(pageSize = pageSize)
}

  1. 惰性计算与缓存,对于计算代价昂贵的数据转换或UI组件,可采用延迟加载和缓存等策略,避免重复计算。

  2. 限制重组范围Compose提供的DisposableEffectSideEffects等API,可用于限制重组的影响范围,避免不必要的重组。

  3. 合理使用Animations,合理使用Jetpack Compose动画API,可以保证动画流畅性,且不会对其他UI区域造成影响。

7.选择性使用ViewsInCompose,对于无法用Compose实现的特殊UI需求(如自定义View),可以考虑将其包装进AndroidView,并于Compose集成使用。

二、LazyColumn与RecyclerView对比

RecyclerView的性能对比,经过多轮的性能测试和数据采集,总结出LazyColumnRecyclerView在大数据列表场景下的一些性能差异:

  1. 内存占用:LazyColumn通常比等效的RecyclerView实现占用更多内存,主要是由于Compose本身的内存模型所致,不过差距在合理的范围内。
  2. CPU开销:LazyColumn渲染时的CPU使用率略高于等效的ViewHolder模型。这是由Compose底层的计算和跟踪机制造成的。
  3. 第一次渲染耗时:LazyColumn第一次渲染列表时的耗时较长,因为需要组合大量项目。而ViewHolder则只需找到对应的View对象并设置数据。
  4. 后续滚动流畅度:滚动过程中,两者的表现接近,都能保持较好的流畅度。不过列表项越复杂,LazyColumn的优势就越小。
  5. 视图层级和测量布局:在复杂布局场景下,ViewHolder的扁平视图层级结构可以带来一定优势,避免了内部的测量和布局开销。
  6. 定制灵活性:相比ViewHolder的高度自由,Compose目前在定制复杂列表视图仍有些局限性,需要谨慎权衡。
  7. 开发体验:在开发体验层,Compose更易于编写、维护和测试,具有响应式编程和声明式UI描述等优势。ViewHolder则需要编写更多样板代码。

总的来说,LazyColumn在性能方面并不比RecyclerView差,而且还有很大的优化空间。实际开发中,我们需要结合具体场景权衡利弊,对于简单列表来说,LazyColumn是更好的选择,但复杂场景下ViewHolder的优势仍然存在。

三、优化Example

下面我将基于最新的Compose 1.6.3版本和Material 3,提供了一个LazyColumn性能优化示例。该示例将包含以下几个部分:

  1. 使用不可变集合
  2. 使用key避免重复渲染
  3. 状态提升
  4. 惰性计算和缓存
  5. 限制重组范围
  6. 合理使用动画
  7. 选择性使用ViewsInCompose
package com.example.jetnotes.ui.screens

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.view.View
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

fun <T> MutableList<T>.toggle(element: T) = if (contains(element)) remove(element) else add(element)

data class MyItem(val id: Int, val name: String)
data class MyItemDetails(val id: Int, val details: String)

class MyViewModel: ViewModel() {
    private val _expandedIds = mutableStateListOf<Int>()
    val expandedIds: List<Int> = _expandedIds
    private val listeners = mutableListOf<(Int) -> Unit>()

    fun toggleExpanded(id: Int) {
        _expandedIds.toggle(id)
        notifyListeners(id)
    }

    fun updateExpandedIds(id: Int) = _expandedIds.toggle(id)

    fun addListener(listener: (Int) -> Unit) = listeners.add(listener)

    fun removeListener(listener: (Int) -> Unit) = listeners.remove(listener)

    private fun notifyListeners(id: Int) {
        listeners.forEach { it(id) }
    }
}

@Composable
fun OptimizedLazyColumnDemo(
    items: List<MyItem> = List(100) { MyItem(it, "Item $it") },
    viewModel: MyViewModel = viewModel()
) {
    val stableItems = items.toList() // 1.使用不可变集合

    LazyColumn(
        content = {
            itemsIndexed(stableItems) { _, item ->
                key(item.id) { // 2.使用key避免重复渲染
                    val isExpanded = viewModel.expandedIds.contains(item.id)

                    MyItemView(
                        item = item,
                        isExpanded = isExpanded,
                        onExpandedChange = {
                            viewModel.toggleExpanded(item.id)
                        }
                    )
                }
            }
        }
    )
}

@Composable
fun MyItemView(
    item: MyItem,
    isExpanded: Boolean,
    onExpandedChange: (Int) -> Unit
) {
    val transition = updateTransition(targetState = isExpanded, label = "MyItemViewTransition")
    val height by transition.animateDp(label = "heightTransition") {
        if (it) 200.dp else 50.dp // 6.合理使用动画
    }

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        onClick = { onExpandedChange(item.id) }
    ) {
        Column(
            modifier = Modifier
                .padding(16.dp)
                .height(height)
        ) {
            Text(text = item.name, style = MaterialTheme.typography.titleLarge, color = Color.Red)
            if (isExpanded) {
                Spacer(modifier = Modifier.height(8.dp))
                val details = remember(item.id) { calculateDetails(item.id) } // 4.惰性计算与缓存
                DisposableEffect(key1 = item.id, effect = { // 5. 限制重组范围
                    // 执行一些副作用操作
                    onDispose {  }
                })
                AndroidView(
                    factory = { context ->
                        MyCustomView(context) // 7.选择性使用ViewsInCompose
                    },
                    update = { view ->
                        view.setData(details)
                    }
                )
            }
        }
    }
}

fun calculateDetails(id: Int): MyItemDetails {
    // 执行一些昂贵的计算操作
    return MyItemDetails(id, "Details for item @id")
}

class MyCustomView(context: Context): View(context) {
    private var data: MyItemDetails? = null
    private val paint = Paint()

    init {
        paint.textSize = 40f
    }

    fun setData(details: MyItemDetails) {
        data = details
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        data?.let {
            canvas.drawText("ID: ${it.id}", 0f, 50f, paint)
            canvas.drawText("Details: ${it.details}", 0f, 100f, paint)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun OptimizedLazyColumnDemoPreview() {
    MaterialTheme {
        OptimizedLazyColumnDemo()
    }
}

以下是对各部分代码的详细解释:

  1. 使用不可变集合

我们使用items.toList()将传入的List<MyItem>转换为不可变列表。这样可以确保在后续重组过程中,列表本身不会被意外修改,从而避免不必要的重组。

  1. 使用Key避免重复渲染

LazyColumnitem作用域内,我们使用key(item.id)为每个项目指定了唯一的key。这可以避免相同数据的项目被记重复渲染,从而减少不必要的计算开销。

  1. 状态提升

我们将列表项的展开/折叠状态提升到MyViewModel中进行集中管理。这样可以避免在每个列表内部都维护自己的状态,从而减少层次的重组。

  1. 惰性计算和缓存

对于计算代价昂贵的calculateDetail操作,我们使用remember函数将结果缓存起来。当相同的item.id再次出现时,就可以直接从缓存中获取结果,避免重复计算。

  1. 限制重组范围

通过LaunchedEffectkey1参数,我们限制了副作用操作的重组范围。只有当item.id发生变化时,相关的骨作用才会被重新执行,从而避免了不必要的重组。

  1. 合理使用动画

使用updateTransitionanimateHeight实现了一个简单的展开/折叠动画效果。这种方式可以确保动画流畅,且不会影响其他UI区域的性能。

  1. 选择性使用ViewsInCompose

对于无法用Compose实现的自定义View,我们使用AndroidView将其包装进来,并与Compose集成使用。这种混合使用方式可以发挥两者的优势,提高灵活性。

最后

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

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

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

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

Jetpack Compose 中,`LazyColumn` 可以使用 `remember` 函数来缓存数据以提高性能。如果数据源发生更改,`LazyColumn` 需要能够检测到这些更改并更新 UI。为此,可以使用 `key` 参数来告诉 `LazyColumn` 如何区分不同的数据项。 以下是一个示例,演示如何使用 `LazyColumn` 和 `remember` 函数来显示一个字符串列表,并在列表中添加或删除字符串时更新 UI: ```kotlin @Composable fun LazyColumnDemo() { // 用 remember 关键字缓存数据源,并使用 key 参数来区分不同的数据项 val items = remember { mutableStateListOf("Item 1", "Item 2", "Item 3") } LazyColumn { items(items.size, key = { index -> // 使用 index 和 item 的哈希值来区分不同的数据项 items[index].hashCode() }) { index -> Text(text = items[index]) // 添加一个按钮,用于在列表中添加新项目 Button(onClick = { items.add("New Item") }) { Text("Add Item") } // 添加一个按钮,用于从列表中删除项目 Button(onClick = { items.removeAt(index) }) { Text("Remove Item") } } } } ``` 在这个示例中,我们使用 `remember` 函数来缓存字符串列表。我们还使用 `key` 参数来告诉 `LazyColumn` 如何区分不同的数据项。在 `LazyColumn` 中迭代数据项时,我们可以使用 `index` 来访问每个项目,并使用 `items[index]` 来显示每个项目的文本。 我们还向 UI 添加了两个按钮,一个用于在列表中添加新项目,另一个用于从列表中删除项目。当我们单击这些按钮时,我们更新 `items` 列表,这将触发 `LazyColumn` 重新运行并更新 UI。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值