当前位置: 首页 > news >正文

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

目录

  • 1.概述
  • 2.确定Composable重组的范围
  • 3.优化重组的性能
    • 3.1 Composable 位置索引
    • 3.2 通过Key添加索引信息
    • 3.3 使用注解@Stable优化重组

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 Jetpack Compose之确定重组范围并优化重组

目录 1.概述2.确定Composable重组的范围3.优化重组的性能3.1 Composable 位置索引3.2 通过Key添加索引信息3.3 使用注解Stable优化重组 1.概述 前面的文章提到Compose的重组是智能的&#xff0c;Composable函数在进行重组时会尽可能的跳过不必要的重组&#xff0c;只对需要变化…...

【JDK 8-集合框架进阶】6.1 parallelStream 并行流

一、parallelStream 并行流 1.1 串行 和 并行的区别 > 执行结果 二、问题 2.1 paralleStream 并行是否一定比 Stream 串行快? 2.2 是否可以都用并行&#xff1f; > 报错 三、实战 > 执行结果 四、总结 一、parallelStream 并行流 多线程并发处理&#xff…...

C语言中结构体,枚举,联合相关介绍

本次重点&#xff1a; 1、结构体 &#xff1a; &#xff08;1&#xff09;结构体类型的声明 &#xff08;2&#xff09;结构的自引用 &#xff08;3&#xff09;结构体变量的定义和初始化 &#xff08;4&#xff09;结构体内存对齐 &#xff08;5&#xff09;结构体传参 …...

【干货】GNSS连续运行基准站网

文章目录 01 ​概述02 基准站建设03 数据中心04 数据通信网络 01 ​概述 1. 基准站网的组成 卫星连续运行基准站网&#xff08;Continuously Operating Reference Stations&#xff0c;缩写 CORS&#xff09;是由若干连续运行基准站及数据中心、数据通信网络组成的&#xff0…...

如何使用iPhone15在办公室观看家里电脑上的4k电影,实现公网访问本地群晖!

如何使用iPhone15在办公室观看家里电脑上的4k电影&#xff1f; 文章目录 如何使用iPhone15在办公室观看家里电脑上的4k电影&#xff1f;1.使用环境要求&#xff1a;2.下载群晖videostation&#xff1a;3.公网访问本地群晖videostation中的电影&#xff1a;4.公网条件下使用电脑…...

LeetCode之26.删除有序数组中的重复项和80.删除有序数组中的重复项II(C++)

文章目录 0 引言1 删除有序数组中的重复项1.1 解题方法1.2 C代码 2 删除有序数组中的重复项II2.1 解题方法2.2 C代码 0 引言 本文主要记录如何解决LeetCode中数组和字符串类别中的26.删除有序数组中的重复项&#xff08;简单&#xff09;及80.删除有序数组中的重复项II &#…...

linux驱动之input子系统简述

文章目录 一、什么是input子系统二、内核代码三、代码分析 一、什么是input子系统 Input驱动程序是linux输入设备的驱动程序&#xff0c;我们最常见的就按键&#xff0c;触摸&#xff0c;插拔耳机这些。其中事件设备驱动程序是目前通用的驱动程序&#xff0c;可支持键盘、鼠标…...

嵌入式裸机架构的探索与崩塌

为什么会想着探索下嵌入式裸机的架构呢&#xff1f;是因为最近写了一个项目&#xff0c;项目开发接近尾声时&#xff0c;发现了一些问题&#xff1a; 1、项目中&#xff0c;驱动层和应用层掺杂在一起&#xff0c;虽然大部分是应用层调用驱动层&#xff0c;但是也存在驱动层调用…...

MySQL高级语句(第二部分)

MySQL高级语句(第二部分)一、视图表 create view1、视图表概述2、视图表能否修改&#xff1f;&#xff08;面试题&#xff09;3、基本语法3.1 创建3.2 查看3.3 删除 4、通过视图表求无交集值 二、case语句三、空值(null) 和 无值(’ ) 的区别四、正则表达式五、存储过程1、简介…...

HTML计时事件(JavaScript)网页电子钟+网页计时器

setTimeout("函数","未来指定毫秒后调用函数"); clearTimeout(setTimeout("函数","未来指定毫秒后调用函数")); <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title>…...

使用群晖实现Videostation电影的大容量存储及分享教程

文章目录 1.使用环境要求2.制作视频分享链接3.制作永久固定视频分享链接 李哥和他的女朋友是一对甜蜜的情侣&#xff0c;但不幸的是&#xff0c;由于工作原因&#xff0c;他们目前分隔两地&#xff0c;无法常常亲密相伴。 这个距离让李哥特别怀念和女朋友一起在电影院观看电影的…...

后端大厂面试-15道题

1. 说说计算机存储结构 计算机存储结构通常包括这几个层次&#xff1a; 主存储器&#xff08;Main Memory&#xff09;&#xff1a;也称为内存&#xff08;RAM&#xff0c;Random Access Memory&#xff09;&#xff0c;主要用于存储当前正在执行的程序和数据。它是计算机中最…...

C++: 冒泡排序(Bubble Sort)

假设你有一列由数字组成的玻璃珠&#xff0c;这些珠子的重量不同&#xff0c;你希望将它们按照重量从轻到重排列。你会这样做&#xff1a; 从左到右&#xff0c;比较相邻的两颗珠子的重量。如果左边的珠子比右边的珠子重&#xff0c;就交换它们的位置。然后&#xff0c;继续向…...

跨域的解决方案

文章目录 概念一、什么是跨域问题二、为什么会发生跨域问题三、跨域解决方案1、JSONP2、添加响应头3、Spring注解CrossOrigin4、配置文件&#xff08;常用&#xff09;5、nginx跨域 概念 一、什么是跨域问题 前端调用的后端接口不属于同一个域&#xff08;域名或端口不同&…...

如何使用Java语言判断出geek是字符串参数类型,888是整数参数类型,[hello,world]是数组参数类型,2.5是双精度浮点数类型?

如何使用Java语言判断出geek是字符串参数类型&#xff0c;888是整数参数类型&#xff0c;[hello,world]是数组参数类型&#xff0c;2.5是双精度浮点数类型&#xff1f; Java是一种静态类型的编程语言&#xff0c;这意味着我们需要在编译时为变量指定具体的类型。但是&#xff…...

9.20华为机试-后端

1、丢失报文的位置 某通信系统持续向外发送报文&#xff0c;使用数组 nums 保存 n个最近发送的报文&#xff0c;用于在报文未达到对端的情况下重发。报文使用序号 sn 表示&#xff0c;序号 sn 按照报文发送顺序从小到大排序&#xff0c;相邻报文 sn 不完全连续且有可能相同。报…...

LC926. 将字符串翻转到单调递增(JAVA - 动态规划)

将字符串翻转到单调递增 题目描述动态规划 题目描述 难度 - 中等 LC926. 将字符串翻转到单调递增(JAVA - 动态规划) 如果一个二进制字符串&#xff0c;是以一些 0&#xff08;可能没有 0&#xff09;后面跟着一些 1&#xff08;也可能没有 1&#xff09;的形式组成的&#xff0…...

【高阶数据结构】哈希的应用 {位图;std::bitset;位图的应用;布隆过滤器;布隆过滤器的应用}

一、位图 1.1 位图概念 面试题 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数中。【腾讯】 遍历查找&#xff1a;内存中无法存放40亿个整数&#xff08;约占内存15-16G&#xff09;&#xff1b;时间复杂…...

金融生产存储亚健康治理:升级亚健康 3.0 ,应对万盘规模的挑战

随着集群规模的不断扩大&#xff0c;硬盘数量指数级上升&#xff0c;信创 CPU 和操作系统、硬盘多年老化、物理搬迁等多种复杂因素叠加&#xff0c;为企业的存储亚健康管理增加了新的挑战。 在亚健康 2.0 的基础上&#xff0c;星辰天合在 XSKY SDS V6.2 实现了亚健康 3.0&#…...

C语言自定义类型讲解:结构体,枚举,联合(2)

&#x1f435;本篇文章将会对位段、枚举和联合的相关知识进行讲解 1. 位段&#x1f4da; 1.1 什么是位段 位段的声明和结构体类似&#xff0c;但是有两点不同&#xff1a; 1.位段的成员必须是int&#xff0c;unsigned int&#xff0c;signed int (C99之后也可以是其他成员&am…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

【杂谈】-递归进化:人工智能的自我改进与监管挑战

递归进化&#xff1a;人工智能的自我改进与监管挑战 文章目录 递归进化&#xff1a;人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管&#xff1f;3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

【算法训练营Day07】字符串part1

文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接&#xff1a;344. 反转字符串 双指针法&#xff0c;两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发&#xff0c;实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构&#xff0c;服务器端使用Java Servlet处理请求&#xff0c;数据库采用MySQL存储信息&#xff0…...

使用LangGraph和LangSmith构建多智能体人工智能系统

现在&#xff0c;通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战&#xff0c;比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...

Golang——9、反射和文件操作

反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一&#xff1a;使用Read()读取文件2.3、方式二&#xff1a;bufio读取文件2.4、方式三&#xff1a;os.ReadFile读取2.5、写…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

OCR MLLM Evaluation

为什么需要评测体系&#xff1f;——背景与矛盾 ​​ 能干的事&#xff1a;​​ 看清楚发票、身份证上的字&#xff08;准确率>90%&#xff09;&#xff0c;速度飞快&#xff08;眨眼间完成&#xff09;。​​干不了的事&#xff1a;​​ 碰到复杂表格&#xff08;合并单元…...