Android Jetpack Compose之确定重组范围并优化重组

1.概述

Compose的重组是智能的,Composable函数在进行重组时会尽可能的跳过不必要的重组,只对需要变化的UI进行重组。那Compose是如何认定UI需要变化呢?或者换句话说Compose是如何确定重组的范围呢。如果重组随意的发生,那么对UI的性能会是一个很不稳定的状态,时而好,时而坏。而且如果编写的UI代码有问题,那么重组将会带来状态的混乱,导致UI显示出错。所以弄清楚Compose重组的范围确定才能更好的避免重组的坑,并且可以针对具体的范围做优化,所以本文将介绍如何确定Compose重组的范围以及重组性能的优化。

2.确定Composable重组的范围

确定重组的范围有助于我们更好的理解ComposeUI的性能优化,下面我们先看一个例子:

    @Composablefun CounterDemo(){Log.d("zhongxj","范围1=>运行")var counter by remember { mutableStateOf(0) }Column {Log.d("zhongxj","范围2=>运行")Button(onClick = {Log.d("zhongxj","onButtonClick:点击按钮")counter ++}){Log.d("zhongxj","范围3=>运行")Text(text = "+")}Text(text = "$counter")}}

在上面的代码中,我们依然使用计数器的例子来验证重组的范围,我们在各个可能发生重组的地方都打上了Log,当点击Button时,计数器counter的状态更新会触发CounterDemo的重组,日志如下图所示:

从图中我们可以看到, Log.d("zhongxj","范围3=>运行") 这行Log并没有打,没有打这行log的原因需要我们了解Compose重组的底层原理:

在Compose中,经过Compose编译器处理后的Composable函数在对State进行读取的同时,能够自动建立关联,在运行过程中,当State变化时Compose会找到关联的代码块并将其标记为Invalid.在下一个渲染帧到来之前,Compose会触发重组并且执行invalid代码块,而Invalid代码块即为下一次重组的范围。能够被标记为Invalid的代码有2个两个要求,一是被标记为Invalid的代码必须时非inline且没有返回值的Composable函数,二是无返回值的Lambda。

那么为啥参与重组的代码块必须是非inline的无返回值函数呢?因为inline函数在编译期会在调用处展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。而有返回值的函数由于返回值会影响调用方,所以必须联通调用方一起参与重组。因此inline的有返回值的函数不能作为Invalid代码块。

而了解了Compose的底层重组原理,我们就可以清楚的知道了只有受到State变化影响的代码块,才会参与到重组。不依赖State的代码则不参与重组,这就是重组的最小化原则。

基于重组最小化原则,我们可以分析下我们计数器例子中的输出结果,其实看了日志发现 Log.d("zhongxj","范围3=>运行")这行日志没有打,也就是说这行日志所在的代码块并没有参与重组,在范围2的作用域中,我们看到了这行代码Text(text = "$counter"),很明显这行代码依赖了counter状态,需要注意的是这行代码并不是读取counter值的意思,它的意思是在范围2的作用域中读取counter的值并传入Text,所以范围2是会参与重组的,日志就输出了Log.d("zhongxj","范围2=>运行"),这时有读者可能会发现,按照重组最小化原则,那么访问counter的最小范围应该是:范围2的作用域呀,为啥范围1的日志也会被打印呢?这里需要回想下咱们之前讲的:最小化范围的定义必须是非inline的composable函数或者lambda。而Column组件是一个inline声明的高阶函数:

所以content内部也会被展开在调用处,所以范围1和范围2就共享了重组的范围,所以输出了Log.d("zhongxj","范围1=>运行")日志,假设将Column换成非inline的Composable,那么Log.d("zhongxj","范围1=>运行")将不会输出,比如换成一个Card组件,读者可以自行试一下。

需要注意的是,Button虽然没有依赖counter,但是范围2的重组会触发Button的重新调用,所以 Log.d("zhongxj","onButtonClick:点击按钮") 也会输出,但是其content内部并没有依赖counter,所以范围3的日志: Log.d(“zhongxj”,“范围3=>运行”) 不会输出。

补充说明: Composable 函数观察State变化并触发重组是在被称为”快照“的系统中完成的,所谓”快照“就是将被访问的状态像拍照一样保存下来,当状态变化时,通知相关的Composable应用的最新状态。”快照“有利于对状态管理进行线程隔离,在多线程场景下的重组有重要的应用

3.优化重组的性能

经过前面的分析,我没了解到了Compose的重组是智能的,遵循范围最小化原则,重组中执行到的Composable只有在其参数发生变化时,才会参与本次重组。

Compose 在执行后会生成一棵视图树,每个Composable对应树上的一个节点,因此Composable 智能重组的本质其实是从树上寻找对应位置的节点并与之进行比较,如果节点未发生变化则不用更新

另外需要注意的是,视图树的实际构建过程比较复杂,Composable执行过程中,先将生成的Composition状态存入SlotTable,然后框架基于SlotTable生成LayoutNode树,并完成最终的界面渲染。所以谨慎的说,Composable的比较逻辑是发生在SlotTable中的。

3.1 Composable 位置索引

在重组的过程中,Composition上的节点可以完成增、删、移动、更新等多种变化,Compose编译器会根据代码调用位置,为Composable生成索引key,并且存入Composition,Composable在执行过程中通过与Key的对比可以知道当前应该执行何种操作。例如下面的示例代码:

    Box {if (state) {val str = remember(Unit) { "call_site_1" }Text(text = str) // Text_of_call_site_1} else {val str = remember(Unit) { "call_site_2" }Text(text = str) // Text_of_call_site_2}}

如上面代码所示:Composable中遇到if/else等条件语句时,会插入startXXXGroup类似的代码,并且通过添加索引Key识别节点的增减,上面的代码中会根据state的不同显示不同的Text,编译器会为if和else分支分别建立索引,当state由true变为false时,Box发生重组,通过key的判断可知,else内的代码需要插入逻辑执行,而if内生成的节点需要被移除。

假设没有编译期的位置索引,而仅仅靠运行时比较,首先执行到 remember(Unit)时,由于缓存原因仍然会返回当前树上存放的str,即call_site_1,接着执行到Text_of_call_site_1,发现与当前树上的节点类型一样,参数str也没有变化,因此会判断为无须重组,那么文本就无法得到更新

所以,综上所述:Composable 在编译期建立索引是保证其重组能够智能且正确执行的基础。这个索引是根据Composable在静态代码中的被调用位置决定的。但是在某些场景中,Composable无法通过静态代码位置进行索引,这时我们需要手动添加索引,便于在重组中进行比较

3.2 通过Key添加索引信息

假设我们现在需要给一个电影列表,然后展示电影的大致信息,代码如下所示:

@Composablefun MoviesScreen(movies:List<Movie>){Column { for (movie in movies){// showMoveCardInfo 无法在编译期间进行索引,只能根据运行时的index进行索引showMoveCardInfo(movie)}}}

如上面的代码所示,基于Movie的名字展示电影的信息,此时无法基于代码中的位置进行索引,只能在运行时基于index进行索引。这样的话索引会根据item的数量发生变化,导致无法准确进行比较。在这种情况下,当重组发生时,新插入的数据会和以前的第一个数据比较,以前的第一个数据会和第二个数据比较,然后以前的第二个数据会被当作新数据插入。结果是所有的item都会发生重组,但是我们期望的行为是,只有新插入的数据需要重组,其他没有变化的数据不应该发生重组,所以我们可以使用key的方法为Composable在运行时手动添加一个索引,如下所示:

@Composablefun MoviesScreen(movies:List<Movie>){Column { for (movie in movies){key(movie.id){ // 使用movie的唯一ID作为Composable的索引showMoveCardInfo(movie)}}}}

使用movie的ID传入Composable做为唯一索引,当插入新数据时,之前对象的索引没有被打乱,仍然可以发挥比较时的锚定作用,所以其他没有发生变化的item就可以不用参与重组

3.3 使用注解@Stable优化重组

Composable是基于参数的比较结果来决定是否重组,也就是说,只有当参与比较的参数对象是稳定的且equals返回true,才认为是相等的。Kotlin中常见的基本类型(Boolean、Int、Long、Float、Char) String,Lambda表达式都可以认为式稳定的,因为都是不可变类型。所以他们的参数比较的结果都式可信的。但是假如参数是可变类型,那么比较的结果将是不可信的。

data class Mutabledata(var data:String)

    @Composablefun MutableDemo(){var mutable = remember { Mutabledata("walt") }var state by remember { mutableStateOf(false) }if(state){mutable.data = "zxj"}Button(onClick = {state = true}){showText(mutable)}}@Composablefun ShowText(mutable:MutableData){Text(text = mutable.data) // 会随着state的变化而变化}

在上面的代码中,MutableData是一个不稳定的对象,因为它有一个Var类型的变量data,当点击按钮改变状态时,mutable会修改data,对于ShowText来说,参数mutable在状态改变前后都指向同一个对象,因此仅仅靠equals判断会认为参数没有发生变化,但实际上测试发现ShowText函数发生了重组,所以Mutabledata参数类型是不稳定的,equals结果不可信。

所以对于一些默认不被认为是稳定类型的,比如interface或者list等集合类,如果能够确保其在运行时的稳定,可以为其添加@State注解,编译器会将这些类型视为稳定类型,从而发挥只能重组的作用,提升性能。代码如下所示:

@Stable
interface UiState<T>{val value:T?val exception:Throwable?val hasError:Booleanget() = exception != null
}

注意: 被添加为@Statble的普通父类、密封类、接口等其派生子类也会被认为时稳定的

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LabVIEW开发实时自动化多物镜云计算全玻片成像装置

LabVIEW开发实时自动化多物镜云计算全玻片成像装置 数字病理学领域正在迅速发展&#xff0c;这主要是由于计算机处理能力、数据传输速度、软件创新和云存储解决方案方面的技术进步。因此&#xff0c;病理科室不仅将数字成像用于图像存档等简单任务&#xff0c;还用于远程病理学…

一些 dp 题

CYEZ 弹珠 key&#xff1a;思维题&#xff0c;完全背包。 先每个组分一个小球。等价于 n − k n-k n−k 拆分为任意个 [ 1 , k ] [1,k] [1,k] 的数的方案数。 本质是根据面积的转换&#xff0c;直观解释&#xff1a; 完全背包即可。代码。 No Bug No Game key&#xff1…

无人直播间

失败&#xff01;&#xff01; 采用 ffmpeg 技术进行推流 推流代码&#xff1a; 【需要将rtmp替换为你的推流地址】 ffmpeg -re -stream_loop -1 -i "rain.mp4" -c copy -f flv ""推流地址获取 以哔哩哔哩为例 点击下方链接 开播设置 - 个人中心 - …

Java编程技巧:文件上传、下载、预览

目录 1、上传文件1.1、代码1.2、postman测试截图 2、下载resources目录中的模板文件2.1、项目结构2.2、代码2.3、使用场景 3、预览文件3.1、项目结构3.2、代码3.3、使用场景 1、上传文件 1.1、代码 PostMapping("/uploadFile") public String uploadFile(Multipart…

麒麟信安服务器操作系统V3.5.2重磅发布!

9月25日&#xff0c;麒麟信安基于openEuler 22.03 LTS SP1版本的商业发行版——麒麟信安服务器操作系统V3.5.2正式发布。 麒麟信安服务器操作系统V3定位于电力、金融、政务、能源、国防、工业等领域信息系统建设&#xff0c;以安全、稳定、高效为突破点&#xff0c;满足重要行…

试图一文彻底讲清 “精准测试”

在软件测试中&#xff0c;我们常常碰到两个基本问题&#xff08;困难&#xff09;&#xff1a; 很难保障无漏测&#xff1a;我们做了大量测试&#xff0c;但不清楚测得怎样&#xff0c;对软件上线后会不会出问题&#xff0c;没有信心&#xff1b; 选择待执行的测试用例&#…

阿里云七代云服务器实例、倚天云服务器及通用算力型和经济型实例规格介绍

在目前阿里云的云服务器产品中&#xff0c;既有五代六代实例规格&#xff0c;也有七代和八代倚天云服务器&#xff0c;同时还有通用算力型及经济型这些刚推出不久的新品云服务器实例&#xff0c;其中第五代实例规格目前不在是主推的实例规格了&#xff0c;现在主售的实例规格是…

MySQL架构 InnoDB存储引擎

1. 什么是Mysql&#xff1f; 我们在开发的时候&#xff0c;我们都需要对业务数据进行存储&#xff0c;这个时候&#xff0c;你们就会用到MySQL、Oracal等数据库。 MySQL它是一个关系型数据库&#xff0c;这种关系型数据库就有Oracal、 MySQL&#xff0c;以及最近很火的PgSQL等。…

Springcloud实战之自研分布式id生成器

一&#xff0c;背景 日常开发中&#xff0c;我们需要对系统中的各种数据使用 ID 唯一表示&#xff0c;比如用户 ID 对应且仅对应一个人&#xff0c;商品 ID 对应且仅对应一件商品&#xff0c;订单 ID 对应且仅对应 一个订单。我们现实生活中也有各种 ID &#xff0c;比如身…

分享78个Python源代码总有一个是你想要的

分享78个Python源代码总有一个是你想要的 源码下载链接&#xff1a;https://pan.baidu.com/s/1ZhXDsVuYsZpOUQIUjHU2ww?pwd8888 提取码&#xff1a;8888 下面是文件的名字。 12个python项目源码 Apache Superset数据探查与可视化平台v2.0.1 API Star工具箱v0.7.2 Archery…

Springcloud:二、Eureka介绍+上手(搭建EurekaServer注册中心+服务注册+服务拉取)

Eureka介绍 Eureka上手 搭建EurekaServer注册中心 在cloud-demo这个maven项目下创建eureka-server模块 引入依赖 在eureka-server模块的pom文件中新增如下代码 <dependencies><dependency><groupId>org.springframework.cloud</groupId><artif…

安卓:解决AndroidStudio导出Unity的Apk(APP)出现2个显示图标

用AndroidStudio打开该项目 实现只保留1个app图标 AndroidManifest.xml的改法如下&#xff1a; <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android" package"com.fru…

uniapp iOS离线打包——原生工程配置

uniapp iOS离线打包&#xff0c;如何配置项目工程&#xff1f; 文章目录 uniapp iOS离线打包&#xff0c;如何配置项目工程&#xff1f;工程配置效果图DebugRelease 配置工程配置 Appkey应用图标模块及三方SDK配置未配置模块错误配置模块TIP: App iOS 离线打包 前提&#xff1a…

EasyX趣味化编程note2,绘制基本图形

创意化编程&#xff0c;让编程更有趣 今天介绍的仍为比较简单的效果&#xff0c;由浅入深来进行学习 介绍每个函数都会附上代码和运行结果&#xff0c;感兴趣的大家可以复制粘贴运行一下看看效果&#xff0c;也可以自己进行改动&#xff0c;非常好玩且加深印象。 上节课的知识…

Java 18的未来:新特性和编程实践

文章目录 引言新特性预览1. 基于值的类的进一步改进2. 模式匹配的增强3. 新的垃圾回收器4. 扩展的模块系统5. 更强大的异步编程 编程实践示例1&#xff1a;基于值的类示例2&#xff1a;模式匹配的增强示例3&#xff1a;新的垃圾回收器 结论 &#x1f389;欢迎来到Java学习路线专…

【Java】建筑工地智慧管理系统源码

智慧工地系统运用物联网信息技术&#xff0c;致力于推动建筑工程行业的建设发展&#xff0c;做到全自动、信息化&#xff0c;智能化的全方位智慧工地&#xff0c;实现工程施工可视化智能管理以提高工程管理信息化水平。 智慧工地平台拥有一整套完善的智慧工地解决方案&#xff…

uni-app:canvas-图形实现1

效果 代码 <template><view><!-- 创建了一个宽度为300像素&#xff0c;高度为200像素的canvas元素。canvas-id属性被设置为"firstCanvas"&#xff0c;可以用来在JavaScript中获取该canvas元素的上下文对象。 --><canvas style"width:200p…

DataX: Ⅱ

序言 这里使用的是master分支,因为官网上并没有release分支,所以先用master分支吧,可能会有问题cuiyaonan2000163.com 参考资料: https://github.com/alibaba/DataXhttps://github.com/alibaba/DataX/blob/master/introduction.md --插件说明文档https://github.com/alib…

5.wifi开发【智能家居:上】,开发准备:智能开关灯,智能采集温湿,智能调彩灯

一。wifi智能家居项目开发 【开发准备1】&#xff1a;继电器控制开发 1.智能开关 器件准备&#xff1a;wifi&#xff08;esp8266&#xff0c;使用CP2102&#xff09;继电器 结果&#xff1a; 2.继电器工作原理 &#xff08;1&#xff09;继电器是一种自动电气开关 &#xff…

代码随想录刷题笔记10——动态规划

动态规划理论基础 动态规划定义 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就区…