Android jetpack Compose使用及性能优化小结

news2024/11/25 11:58:06
大概在两年前在星河集团的左邻右家项目中,我就接触到了jetpack Compose,并且还项目中在逻辑简单的页面,使用了compose去实现。当时觉得很新颖,实践中也感觉到,这种响应式的,与当时的Vue/微信小程序/Flutter中思想大同小异,可能是未来的一种原生写UI的趋势。在现在的每记和脚印项目中,新实现的页面,都会优先考虑用Compose去实现。然而,Compose的一些性能优化点及注意点,也是做为开发人员需要熟悉的,今天将做一个小的总结。

一、声明式 vs 指令式编程

1、定义
无论是官网文档还是介绍Compose的优点时,都会说到Compose是声明式的。我们来回顾下,在wiki上有着如下定义:

声明式编程(英语:Declarative programming)或译为声明式编程,是对与命令式编程不同的编程范型的一种合称。它们建造计算机程序的结构和元素,表达计算的逻辑而不用描述它的控制流程[1]。

指令式编程(英语:Imperative programming);是一种描述电脑所需作出的行为的编程范型。几乎所有电脑的硬件都是指令式工作;几乎所有电脑的硬件都是能执行机器语言,而机器代码是使用指令式的风格来写的。

通俗的来说就是:声明式编程是一种把程序写成描述结果的形式,而不是如何获得结果的形式。它主要关注结果,而不是实现细节。声明式编程的代码通常更简洁,更容易理解和维护。

命令式编程则是一种把程序写成指令的形式,告诉计算机如何实现结果。它更加关注细节,如何实现任务。命令式编程的代码通常更长,更难理解和维护。

2、个人理解

Compose其实就是UI框架,它最主要的功能就是让开发人员更加快速的实现 页面逻辑&交互效果 这是目的。

对于传统的XML来说,我们通过请求去服务器获取数据,请求成功后,我们需要findViewById找到页面元素View,再设置View的属性,更新页面展示状态。整个过程是按 http请求 -> 响应 -> 寻找对应View -> 更新对应View按部就班就地执行,这种思想就是命令式编程。

但是Compose描述为 http请求 -> 响应 -> 更新mutableData -> 引用对应数据的View自动重组,整个过程不需要我们开发去写更新UI的代码(发出命令),而是数据发生改变,UI界面自动更新,可以理解为声明式。

二、Compose优势

目前对于我的体验感受来说,Compose的优势体现在以下几个点:

  • 页面架构清晰。对比以前mvp,mvvm或结合viewbinding,少去了很多接口及编写填充数据相关的代码

  • 动画API简单好用。强大的动画支持,使得写动画非常简单。

  • 开发效率高,写UI速度快,style、shape等样式使用简单。

  • 另外、还有一些官方优势介绍

三、Compose 的重组作用域

虽然Compose 编译器在背后做了大量工作来保证 recomposition 范围尽可能小,我们还是需要对哪些情况发生了重组以及重组的范围有一定的了解 。

假设有如下代码:

@ComposablefunFoo() {
    var text by remember { mutableStateOf("") }
    Log.d(TAG, "Foo")
    Button(onClick = {
        text = "$text$text"
    }.also { Log.d(TAG, "Button") }) {
        Log.d(TAG, "Button content lambda")
        Text(text).also { Log.d(TAG, "Text") }
    }
}
复制代码

其打印结果为:

D/Compose: Button content lambda
D/Compose: Text
复制代码

按照开发经验,第一感觉会是,text变量只被Text控件用到了。

分析一下,Button控件的定义为:

参数 text 作为表达式执行的调用处是 Button 的尾lambda,而后才作为参数传入 Text()。 所以此时最小重组范围是 Button 的 尾lambda 而非 Text()

另外还有两点需要关注:

  • Compose 关心的是代码块中是否有对 state 的 read,而不是 write。

  • text 指向的 MutableState 实例是永远不会变的,变的只是内部的 value

重组中的 Inline 陷阱!
非inline函数 才有资格成为重组的最小范围,理解这点特别重要!

我们将代码稍作改动,为 Text() 包裹一个 Box{...}

@ComposablefunFoo() {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text$text" }) {
        Log.d(TAG, "Button content lambda")
        Box {
            Log.d(TAG, "Box")
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}
复制代码

日志如下:

D/Compose: Button content lambdaD/Compose: BoxD/Compose: Text复制代码

要点

  • Column、Row、Box 乃至 Layout 这种容器类 Composable 都是 inline 函数,因此它们只能共享调用方的重组范围,也就是 Button 的 尾lambda

如果你希望通过缩小重组范围提高性能怎么办?

@ComposablefunFoo() {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text$text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}

@ComposablefunWrapper(content: @Composable () -> Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}
复制代码
  • 自定义非 inline 函数,使之满足 Compose 重组范围最小化条件。

四、Compose开发时,提高性能的关注点

当 Compose 更新重组时,它会经历三个阶段(跟传统View比较类似):

  • 组合:Compose 确定要显示的内容 - 运行可组合函数并构建界面树。

  • 布局:Compose 确定界面树中每个元素的尺寸和位置

  • 绘图:Compose 实际渲染各个界面元素。

基于这3个阶段,尽可能从可组合函数中移除计算。每当界面发生变化时,都可能需要重新运行可组合函数;可能对于动画的每一帧,都会重新执行您在可组合函数中放置的所有代码。

1、合理使用 remember

它的作用是:

  • 保存重组时的状态,并可以有重组后取出之前的状态

引用官方的栗子🍭:

@ComposablefunContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}
复制代码
  • LazyColumn在滑动时,会使自身状态发生改变导致ContactList重组,从而contacts.sortedWith(comparator)也会重复执行。而排序是一个占用CPU算力的函数,对性能产生了较大的影响。

正确做法:

@ComposablefunContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, sortComparator) {
        contacts.sortedWith(sortComparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
          // ...
        }
    }
}
复制代码
  • 使用remember会对排序的结果进行保存,使得下次重组时,只要contacts不发生变化 ,其值可以重复使用。

  • 也就是说,它只进行了一次排序操作,避免了每次重组时都进行了计算。

提示:

  • 更优的做法是将这类计算的操作移出Compose方法,放到ViewModel中,再使用collectAsState或LanchEffect等方式进行观测自动重组。

2、使用LazyColumn、LazyRow 等列表组件时,指定key

如下一段代码,是一个很常见的需求(from官网):

🍔NoteRow记录每项记录的简要信息,当我们进入编辑页进行修改后,需要将最近修改的一条按修改时间放到列表最前面。这时,假若不指定每项Item的Key,其中一项发生了位置变化,都会导致其他的NoteRow发生重组,然而我们修改的只是其中一项,进行了不必要的渲染。

@ComposablefunNotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}
复制代码

正确的做法:

  • 为每项Item提供 项键,就可避免其他未修改的NoteRow只需挪动位置,避免发生重组

@ComposablefunNotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
             key = { note ->
                // 为每项Item提供稳定的、不会发生改变的唯一值(通常为项ID)
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}
复制代码
3、使用 derivedStateOf 限制重组

🍗假设我们需要根据列表的第一项是否可见来决定划到顶部的按钮是否可见,代码如下:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}
复制代码
  • 由于列表的滑动会使listState状态改变,而使用showButton的AnimatedVisibility会不断重组,导致性能下降。

🍟解决方案是使用派生状态。如下 :

val listState = rememberLazyListState()

LazyColumn(state = listState) {
  // ...
  }

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}
复制代码
  • 派生状态,可以这样理解,只有在derivedStateOf里的状态发生改变时,只关注和派发对UI界面产生了影响的状态。这样AnimatedVisibility只会在改变时发生重组。对应的应用场景是,状态发生了改变,但是我们只关注对界面产生了影响的状态进行分发,这种情况下,就可以考虑使用。

4、尽可能延迟State的读行为

之前我们提到,对于一个Compose页面来说,它会经历以下步骤:

  • 第一步,Composition,这其实就代表了我们的Composable函数执行的过程。

  • 第二步,Layout,这跟我们View体系的Layout类似,但总体的分发流程是存在一些差异的。

  • 第三步,Draw,也就是绘制,Compose的UI元素最终会绘制在Android的Canvas上。由此可见,Jetpack Compose虽然是全新的UI框架,但它的底层并没有脱离Android的范畴。

  • 最后,Recomposition,也就是重组,并且重复1、2、3步骤。

尽可能推迟状态读取的原因,其实还是希望我们可以在某些场景下直接跳过Recomposition的阶段、甚至Layout的阶段,只影响到Draw。

🍿分析如下代码:

@ComposablefunSnackDetail() {
    // Recomposition Scope// ...
    Box(Modifier.fillMaxSize()) {  Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value) // 1,状态读取// ...
    } 
// Recomposition Scope End
}

@ComposableprivatefunTitle(snack: Snack, scroll: Int) {
    // ...val offset = with(LocalDensity.current) { scroll.toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状态使用
    ) {
        // ...
    }
}
复制代码

上面的代码有两个注释,注释1,代表了状态的读取;注释2,代表了状态的使用。这种“状态读取与使用位置不一致”的现象,其实就为Compose提供了性能优化的空间。

那么,具体我们该如何优化呢?简单来说,就是让:“状态读取与使用位置一致”

改为如下 :

// 代码段12@ComposablefunSnackDetail() {
    // Recomposition Scope // ...

    Box(Modifier.fillMaxSize()) {
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value } // 1,Laziness// ...
    } 
    // Recomposition Scope End
}

@ComposableprivatefunTitle(snack: Snack, scrollProvider: () -> Int) {
    // ...val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状态读取+使用
    ) {
    // ...
    }
}
复制代码

理解:由于我们将scroll.value变成了Lambda,所以,它并不会在composition期间产生状态读取行为,这样,当scroll.value发生变化的时候,就不会触发「重组」,这就是 延迟 的意义。

五、小结

其实以上案例优化的点在本质上,都是在践行:状态读取与使用位置一致的原则。但是需要我们对Compose的底层原理,快照系统,还有ScopeUpdateScope有一定的了解。

Android核心知识点笔记:

Android开发核心知识点笔记

Android Framework核心知识点笔记

音视频开发笔记,入门到高级进阶

Android Flutter核心知识点笔记与实战详解

性能调优核心知识点笔记

Android开发高频面试题,25个知识点整合

Android开发核心架构知识点笔记

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383653.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JDK动态代理详解

1.什么是动态代理 可能很多小伙伴首次接触动态代理这个名词的时候&#xff0c;或者是在面试过程中被问到动态代理的时候&#xff0c;不能很好的描述出来&#xff0c;动态代理到底是个什么高大上的技术。不方&#xff0c;其实动态代理的使用非常广泛&#xff0c;例如我们平常使用…

考研复试——计算机网络

文章目录计算机网络1. 说下计算机网络体系结构2. 说一下每层协议有哪些&#xff1f;3. 数据在各层之间是如何传输的呢&#xff1f;4. [从浏览器地址栏输入 url 到显示主页的过程&#xff1f;](https://blog.csdn.net/weixin_46351593/article/details/115386029)5. 说说 HTTP 常…

Guitar Pro8中文版打谱编曲软件

许多打谱编曲软件中都有吉他乐器的插件&#xff0c;插入音轨即可使用&#xff0c;除此以外&#xff0c;还有一款专门针对吉他的音乐软件&#xff0c;就是Guitar Pro。Guitar Pro是吉他类音乐软件中比较有代表性的&#xff0c;从开发至今不断更新优化&#xff0c;目前的软件版本…

如何远程访问公司的电脑?

是否可以从其他地方远程访问公司的电脑&#xff1f;答案是肯定的。一个可靠的远程桌面工具可以让您从另一台设备远程访问您的工作电脑&#xff0c;无论位置如何&#xff0c;无论您是在咖啡厅、酒店还是家中&#xff0c;您都可以从另一台电脑或移动设备远程访问您工作的电脑以处…

【算法基础】深度优先搜索(DFS) 广度优先搜索(BFS)

一、DFS & BFS 1. 深度优先搜索DFS 深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。 2. 广度优先搜索BFS 广度优先搜索较之深度优先搜索之不同在于,深度…

TCP内部的十大核心机制

文章目录1、确认应答机制2、超时重传机制3、连接管理机制1、三次握手2、四次挥手4、滑动窗口机制5、流量控制机制6、拥塞控制7、延时应答机制8、捎带应答机制9、面向字节流10、特殊情况1、确认应答机制 TCP是可靠传输&#xff0c;那么TCP协议能够实现可靠传输的核心机制就是确…

Anker推出Security SmartTrack卡,详谈苹果Find My技术

Anker 旗下品牌 Eufy 近日在欧洲、美国市场推出了 Security SmartTrack 卡。其工作原理和苹果 AirTag 类似&#xff0c;允许用户通过手机定位追踪到绑定的物品。 Security SmartTrack卡通过蓝牙连接&#xff0c;范围为 260 英尺&#xff08;约 80 米&#xff09;。采用防水设…

跨境数据传输是日常业务中经常且至关重要的组成部分

跨境数据传输是日常业务中经常且至关重要的组成部分。在过去的20年中&#xff0c;由于全球通信网络和业务流程的发展&#xff0c;全球数据流的模式已迅速发展。随着数据从数据中心移到数据中心和/或跨边界移动&#xff0c;安全漏洞已成为切实的风险。有可能违反国家和国际数据传…

INOBITEC PRO DICOM VIEWER 2.9.0 Crack

inobitec dicom高级 3D 重建&#xff0c;以 OBJ、STL、PLY 格式导出表面&#xff0c;先进的多计划重建&#xff0c;添加标记和标记线&#xff0c;将系列与高级工具相结合&#xff0c;具有多种选择的虚拟内窥镜检查&#xff0c;从视口录制视频&#xff08;仅限 64 位版本&#x…

CSS的6个新特性

1、容器查询&#xff08;Container Queries&#xff09; 容器查询container类似于媒体查询media&#xff0c;区别在于查询所依据的对象不同。媒体查询依据的是浏览器的视窗大小&#xff0c;容器查询依据的是元素的父元素或者祖先元素的大小。 有关容器查询的属性一共有三个&a…

vue2的动画和过渡效果

文章目录过渡 & 动画Transition 组件基于 CSS 的过渡效果CSS 过渡类名 class为过渡效果命名CSS 过渡 transition实例1&#xff1a;实例2&#xff1a;CSS 动画自定义过渡的类名同时使用 transition 和 animation深层级过渡与显式过渡时长性能考量JavaScript 动画可复用过渡效…

STM32 HAL库硬I2C的TOF050C模块

前言最近在倒腾毕业设计&#xff0c;需要用到TOF050C&#xff0c;但是现有的案例都是软IIC&#xff0c;并且还是基于STM32F103的&#xff0c;笔者用的STM32F767&#xff0c;没有GPIO->CRH寄存器。问题来了&#xff0c;如果我每次都要去看寄存器手册属实费时间&#xff0c;这…

案例08-让软件的使用者成为软件的设计者

一&#xff1a;背景介绍 对于需求的开发每天可能都会有上线的情况&#xff0c;为了防止每次上线拉取代码或者修改配置而引发的冲突以及发生了冲突应该找谁一起确定一下代码留下那一部分的情况。所以在开发的群中会有一个表格来记录每个需求上线修改的环境、是否修改数据库、是否…

【论文阅读】Research on video adversarial attack with long living cycle

论文链接&#xff1a;添加链接描述 Method OPTIMIZATION PROBLEM DESCRIPTION XXX是浮点数域中的对抗视频示例&#xff0c;XcX_cXc​表示encoded的视频对抗示例。设EXˆ−XE Xˆ−XEXˆ−X表示在对抗中增加的扰动&#xff0c;EcXc−XE_c Xc − XEc​Xc−X表示视频压缩编码损…

【线性筛+DP】最大和

看错题了&#xff0c;呃呃&#xff0c;其实就是个简单DP最大和 - 蓝桥云课 (lanqiao.cn)题意&#xff1a;思路&#xff1a;设dp[i]为以1为终点的最大和&#xff0c;然后枚举状态和决策就行了主要是线性筛的应用&#xff0c;它可以预处理出一个数的最小质因子是多少Code&#xf…

如何用BurpSuite抓取手机数据包

文章目录前言准备工具Burp Suite物理机或虚拟机(移动设备)手机抓包网络环境开启burp并设置代理手机配置代理安装Burp证书开始抓包踩坑后记前言 最近挖了一波src&#xff0c;挖来挖去发现有很多公众号或者app没有测试&#xff0c;这就需要Burp能够抓取手机的数据包了&#xff0…

九州云出席全球人工智能开发者先锋大会,圆桌论道开源未来

2月25日-26日&#xff0c;2023年全球人工智能开发者先锋大会&#xff08;GAIDC&#xff09;在临港成功召开。本届盛会以“向光而行的开发者”为主题&#xff0c;汇集政府职能部门领导、国内外知名专家学者、具有国际影响力的开源创业者&#xff0c;聚焦前瞻探索、开源开放、人才…

Linux gcc/g++编译链接头文件和库(动态库.so 和 静态库.a)

最近在学习log4cpp库时&#xff0c;使用g去编译&#xff0c;却发现自己不会链接...&#xff0c;这哪能行&#xff0c;于是网上钻研&#xff0c;终于解决&#xff0c;现在记录下来分享给遇到同样问题的人。 gcc和g类似&#xff0c;这里就以g为例&#xff01; 刚好用到的log4cpp…

测开:vue入门(1)

目录 一、背景 二、介绍 三、创建项目 3.1 创建vue项目 方式二&#xff1a;直接在html页面中&#xff0c;引入vue 3.2 直接在html页面中&#xff0c;引入vue 3.2.1 引入在线的vue&#xff08;方式一&#xff09; 3.2.2 将vue 下载到本地&#xff08;方式二&#xff09; …

代码随想录算法训练营第二天| 977. 有序数组的平方、209. 长度最小子数组、59.螺旋矩阵II

977 有序数组的平方题目链接&#xff1a;977 有序数组的平方介绍给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。思路看到题目的第一反应&#xff0c;首先负数的平方跟正数的平方是相同的&…