Jetpack Compose -> 重组的性能风险和优化

前言


上一章我们讲解了 Jetpack Compose -> mutableStateOf 状态机制的背后秘密 本章我们讲解下重组的性能风险以及怎么优化;

重组的性能风险


前面我们一直在讲重组(ReCompose) 的过程,在使用 mutableStateOf() 以及对于 List 和 Map 在使用 mutatbleStateListOf()、mutableStateMapOf() 也能监听到内部状态变化,如果对于 List 和 Map 使用的是 mutableStateOf() 只能触发这个对象变化的监听;

重组其实分为:触发重组和重组,这是两个过程,触发重组是某个变量发生改变之后,Compose 去把已经组合好的那些部分重新的 Compose 一次,这个所谓的组合好的部分就是之前说的组合过程的结果;也就是那个稍后被拿去组合、测量、绘制的结果,它在相关的变量改变之后,是需要重新组合过程,重新生成结果的;

触发重组,就是 ReCompose Scope 重组范围;重组是在下一帧的时候去调用这些失效了的 compose 代码,来重新生成组合的过程;

因为 Column 函数是一个内联函数,所以 Column 函数在编译后会被抹除掉,也就是说,如果我们在使用下面的逻辑的时候

private var number by mutableStateOf(1)@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Android_VMTheme {Surface {println("Recompose 测试范围1")Column {println("Recompose 测试范围2")Text(text = "当前数值是: $number", modifier = Modifier.clickable {number ++})}}}}
}

Column 会被抹除掉,而是直接将 Text 放到那里,也就是说如果发生了 ReCompose 的时候,是会把 Column 前后范围内的都会触发 Recompose;

也就是说当我们点击 Text 触发 number++ 的时候,会再一次打印 “Recompose 测试范围1” 和 “Recompose 测试范围2”;

这其实就是重组的性能风险;一个小的改动,触发了大面积的 ReCompose,这就造成了计算资源浪费;

我们来继续验证下这个结论,假设我们有下面这样的一段代码

private var number by mutableStateOf(1)@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Android_VMTheme {Surface {println("Recompose 测试范围1")Column {testPerformance()println("Recompose 测试范围2")Text(text = "当前数值是: $number", modifier = Modifier.clickable {number ++})}}}}
}@Composable
fun testPerformance() {println("Recompose 测试范围 performance")Text(text = "test")
}

按照上面的结论,当我们执行这段代码的时候,应该会打印 “Recompose 测试范围1” "Recompose 测试范围 performance 和 “Recompose 测试范围2”;

我们来运行看下,当我们点击 Text 的时候,发现并没有打印 “Recompose 测试范围 performance” 这一行输出,那么这是为什么呢?

难道这个 testPerformance 函数没有被调用吗?不是的,它被调用了,但是内部的逻辑没有执行,我们前面讲到过 Compose 的编译过程是由它的编译器 插件来干预的,这个干预过程会修改我们的 Composable 函数,会给函数增加一些参数,例如 Composer 函数会被添加进去,也会给你的函数添加一些条件判断进去,判断这个函数跟上一次调用传入的参数有没有改变,如果没有改变,就会跳过这个函数的内部执行逻辑,这是 Compose 的一种优化,它会避免在 ReCompose 的时候一些没必要的执行;

我们来看这个 testPerformance 函数,它在第二次被调用的时候,它的函数参数并没有发生变化,所以它内部的逻辑不会再执行;

如果我们给 testPerformance 函数增加一个参数

@Composable
fun testPerformance(text: String) {println("Recompose 测试范围 performance")Text(text = "test $text")
}

调用的时候,传入 number;

testPerformance(number.toString())

然后我们来重新执行一下,可以看到,结果是我们预期的结果了;Text 地方因为 number 的改变会被标记一次失效,同时 testPerformance 的调用地方因为也用到了 number,也会被标记一次失效,虽然标记了两次,但是这个是没有关系的,它只会执行一次重组,因为标记和重组是两个过程,它们是分开的,而且标记是个很轻的工作,它是不耗费什么计算资源的,所以不用担心性能,只会重组一次;

当传入一个 number 之后,这个 testPerformance 的重组就会从被动执行变成主动标记失效并执行的过程了;从执行角度来看是一样的,从标记角度来看,是从被动标记变成了主动标记的过程;

当 testPerformance 执行 ReCompose 的时候,Compose 发现它的函数变了,Compose 就会在第二次进入这个代码,所以打印就会执行;

这是 Compose 中很重要的一个性能优化点;那么问题来了,『这个性能优化是 Compose 相对传统 View 系统的写法的优势吗?』

答案显然不是的,传统写法是手动更新的,Compose 是自动更新的,而自动更新就会触发一个更新范围过大超过需求的问题,从而需要让你的框架去做这种跳过没有必要的更新的优化;这是针对过度更新的问题的优化,而不是相对传统 View 系统写法的优势

Compose 的重组在函数没有变化的时候,跳过函数的内部代码的执行,那肯定需要在 Recompose 的时候做一个对比,去比对 Compose 函数的值是否发生了改变,这个是否改变的判断,它是靠什么来判断的呢?

Structual Equality 结构性相等(Kotlin 的 ==)


这里额外提一个知识点,在 Kotlin 中 == 等于 Java 中的 equals,而 === 才等于 Java 的 ==;我们来验证下 Compose 在重组的时候是否是依赖的这种结构性相等来做出是否重组的决定;

我们来声明一个 data class;

data class User(val name: String)

testPerformance 修改如下:

@Composable
fun testPerformance(user: User) {println("Recompose 测试范围 performance")Text(text = "test $user.name")
}

调用的地方修改为

private var number by mutableStateOf(1)
var user = User("Mars")@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Android_VMTheme {Surface {println("Recompose 测试范围1")Column {testPerformance(user)println("Recompose 测试范围2")Text(text = "当前数值是: $number", modifier = Modifier.clickable {number ++user = User("Mars")})}}}}
}

当我们执行点击事件,number++的时候,我们给 user 重新赋了一次值,当 ReCompose 的时候,testPerformance(user) 就会使用这个新创建的 user,如果不是使用的结构性相等的话,那么就会执行 testPerformance 中的打印,如果使用的是结构性相等,则不会打印;

我们运行看下,可以看到,并没有打印 testPerformance 中的日志,说明 Compose 在 ReCompose 使用的是结构性相等来判断是否要重组;

可靠的类 & 不可靠的类

我们接下来做另一个改动,把 name 的修饰符改成 var,其他地方不做改动;

data class User(var name: String)

我们来运行看下,可以看到,这次直接打印了 testPerformance 中的日志,说明 Compose 在 ReCompose 的时候发生了重组;

那么这是为什么呢?因为当我们使用 var 修饰符的时候,Kotlin 就认为这个类不可靠了,对于可靠的类,Compose 使用结构性相等来判断是否发生了改变,对于不可靠的类,Compose 就不判断,直接进入 Composeable 函数的内部,无脑执行了;

那么,问题来了,为什么一个 var 关键字就把这个类变成了不可靠的类了呢?

我们先来把上面这段代码做一个小小的改动;

private var number by mutableStateOf(1)
val user1 = User("Mars")
val user2 = User("Mars")
var user = user1@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Android_VMTheme {Surface {println("Recompose 测试范围1")Column {testPerformance(user)println("Recompose 测试范围2")Text(text = "当前数值是: $number", modifier = Modifier.clickable {number ++user = user2})}}}}
}
data class User(var name: String)

我们来创建两个新的 User 分别是 user1 和 user2,这样写对于程序执行的结果是不变的, 当我们点击 Text 的时候,testPerformance 在 ReCompose 的时候也会改变,它的参数从 user1 变成了 user2,这个时候在 ReCompose 的时候,是会进入这个 testPerformance 函数内部的,因为 User 类的 name 是 var 类型的,一个神奇的存在,var 就会让其重组,我们假设它不会发生重组的场景下来推演下它会发生什么;

假设在重组的过程中,认为这个 user 没有改变,理由是它们通过 equals 判断认为是同一个对象,所以认为没变,就跳过了 testPerformance 这个函数的内部执行,这样显示不会出问题,但是如果在这之后,程序又在其他地方执行了一些其他逻辑,从其他地方把 user2 的 name 的值做了修改,这个是不会触发重组的,因为 user1 和 user2 并没有使用 mutableStateOf,但是如果又由于其他原因触发了 ReCompose 的行为,但是不是从外面这种捎带着往里的触发 testPerformance 的重组,而是直接触发了 testPerformance 内部的重组,那么当它内部独立的发生了 ReCompose 的时候,它内部显示的文字是不会改变的,因为它内部始终监听的是 user1 对象,虽然在
点击事件中 testPerformance(user1) 的 user1 被替换成了 user2 但是内部并没有发生重组,也就是对于 testPerformance 内部来说,它一直观测的都是 user1 这个旧的值,这样的话就算 testPerformance 内部发生了独立的 ReCompose,并且它应该观测的 user2 的 name 值也发生了改变,但是它内部的显示并不会去显示这个 user2 的 name 的最新值,而是显示那个本来应该被抛弃的 user1 的 name 的值;

简单来说:当 testPerformance 在 ReCompose 的时候,参数里面的 user 换成一个新的对象的时候,如果 Compose 用 equals 判断出来新对象和老对象是相等的,那就不进入 testPerformance 的内部代码了,虽然当下没有显示问题,并且看似节约了性能,但是会导致函数内部的后续的监听全部失效了,都监听了错的老的对象,而没有监听正确的新的对象,造成未来的显示问题;

所以 当我们使用 var 的时候,Compose 就无脑的直接进入了,它认为这个发生了改变;val 关键字修饰的字符串它的值是不可以修改的,而 User 类它的内部只有 name 这一个属性,如果这个 name 是不可变的,那么这个创建出来的 User 对象也是不会改变的;

也就是说:如果能保证现在相等,以后也相等,那么就不进入,如果不能保证现在相等,以后也相等,那么就无脑进入;

到这的时候,可能好多人就有疑问了,这个优化岂不是根本用不到,我们大部分使用的是 var 类型,还是会造成性能损耗;这其实是一个存在了好久的问题;

我们都使用过 HashMap,它在 put 元素的时候,通过 hashCode() 和 equals() 来判断 key 的冲突,当我们使用一个对象作为 key 的时候:

HashMap<User, String> hashMap = new HashMap();
User user = new User("Mars"); // 自己实现了 hashCode() equals()
hashMap.put(user, "1")

当我们使用一个对象作为 key 的时候,一定要确定它的 hashCode 值是不可变的,如果我们使用的是一个 data class,它的 hashCode 和 equals 都是和它内部的值有关系的,如果我们使用一个 data class 来作为 key 就要保证它的值是不可变的,

@Stable


那么针对上面的问题,我们怎么解决呢? Compose 提供了一种方案,『@Stable』注解,如果你给 User 类添加这么一个注解,Compose 在 ReCompose 的时候就会跳过;

@Stable
data class User(var name: String)

这个 @Stable 是一个稳定性标记,加上这个注解就是在告诉 Compose 编译器插件,这个类型是可靠的,不用检查,由人工来保证;

但是人工保证并不能做到绝对,程序还是可能会出现问题,那么我们怎么处理呢?就是让它不相等,也就是我们不去重写 equals 方法,而是采用它本身的 equals 逻辑;

@Stable
class User(var name: String)

当我们使用的是同一个对象的时候,可以让 Compose 编译器插件不执行检查;

private var number by mutableStateOf(1)
// val user1 = User("Mars")
// val user2 = User("Mars")
var user = User("Mars")@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Android_VMTheme {Surface {println("Recompose 测试范围1")Column {testPerformance(user)println("Recompose 测试范围2")Text(text = "当前数值是: $number", modifier = Modifier.clickable {number ++// user = user2})}}}}
}

当我们点击 Text 的时候,没有给 user 重新赋值,但是我们使用 『@Stable』注解标记了这个 User 类,那么就不会执行内部的检查;

另外,@Stable 的稳定,需要满足下面三点

  1. 现在相等就永远相等;
  2. 当公开属性改变的时候,要通知到用这个属性的 Composition
  3. 公开属性,也必须全都是可靠类型,或者说稳定类型

那么怎么通知到 Composition 呢?很简单,就是通过 mutableStateOf;

@Stable
class User(name: String) {var name by mutableStateOf(name)
}

针对第三点,我们来看下面的代码

class Company(var address: String)class User(name: String, company: Company) {var name by mutableStateOf(name)var company by mutableStateOf(company)
}

因为 company 是一个不稳定的属性,所以它就会导致 User 成为一个不稳定的属性,哪怕是使用了 mutableStateOf;

而 Compose 只会判断第二条,只要满足第二条,它就会认为稳定;

好了,今天的 Compose 就到这里吧;

下一章预告


derivedStateOf 与 remember() 有什么区别?

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

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

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

相关文章

Excel模板导入、导出工具类

1.引入maven依赖&#xff0c;利用hutool的excel读取 Hutool-poi对excel读取、写入 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency> <depen…

Linux之安装Nginx

目录 传送门前言一、快速安装二、反向代理语法1、基本语法2、location语法1. 基本语法2. 匹配规则3. 修饰符4. 权重5. 嵌套location6. 其他指令7.案例 三、配置反向代理 传送门 SpringMVC的源码解析&#xff08;精品&#xff09; Spring6的源码解析&#xff08;精品&#xff0…

Java 海报-基于Graphics2D 实现个人头像的圆形裁剪

效果&#xff1a; 代码&#xff1a; private static BufferedImage resizeAndClipToCircle(BufferedImage image, int size) {// 缩小图片BufferedImage resizedImage new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);Graphics2D g2d resizedImage.createGraphi…

5.组合与继承

1.面向对象 在C中&#xff0c;面向对象&#xff08;Object-Oriented&#xff09;是一种程序设计范式&#xff0c;它使用“对象”来设计应用程序和软件。面向对象编程&#xff08;OOP&#xff09;的核心概念包括类&#xff08;Class&#xff09;、对象&#xff08;Object&#x…

Vivado综合属性SRL_STYLE怎么用?

“SRL_STYLE”属性是Vivado中用于控制移位寄存器&#xff08;Shift Register Logic, SRL&#xff09;映射方式的关键属性。 本文将详细介绍SRL_STYLE的工作原理、可选值及其在实际设计中的应用代码示例。 一、什么是SRL_STYLE&#xff1f; SRL_STYLE属性用于指导Vivado综合工…

方便快捷!使用Roboflow进行数据增强(附详细操作)

最近使用自定义数据集训练yolov8模型的时候突然发现一件很令人头疼的事情。那就是&#xff0c;数据集中图片太少了。于是想通过数据增强的方法扩大数据集。 通过查阅资料发现&#xff0c;大部分人都是用python中的imgaug库进行图像处理&#xff1b;这种方法最大的不便就是需要转…

【贪心算法】Leetcode 55. 跳跃游戏【中等】

跳跃游戏 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&…

RTT设备驱动框架学习(GPIO)

基类对象&#xff0c;定义在rtdef.h中 struct rt_object {char name[RT_NAME_MAX];rt_uint8_t type; //设备类Devicert_uint8_t flag;rt_list_t list; }; //同一类对象挂接在一个链表上设备基类&#xff0c;定义在rtdef.h中&#xff08;属于设备接口层&#xff09; struct rt…

如何通过需求跟踪矩阵加强需求管理?

需求跟踪矩阵是强化需求管理的有效工具&#xff0c;它能直观地展示需求与项目各阶段产出物之间的映射关系&#xff0c;确保每个需求从提出到最终实现都得到严密的跟踪与控制&#xff0c;从而提高需求质量和开发效率&#xff0c;保障项目顺利交付。 如果没有需求跟踪矩阵&#x…

多图详解VSCode搭建Python开发环境

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【Elasticsearch】Elasticsearch 从入门到精通(二):基础使用

《Elasticsearch 从入门到精通》共包含以下 2 2 2 篇文章&#xff1a; Elasticsearch 从入门到精通&#xff08;一&#xff09;&#xff1a;基本介绍Elasticsearch 从入门到精通&#xff08;二&#xff09;&#xff1a;基础使用 &#x1f60a; 如果您觉得这篇文章有用 ✔️ 的…

用Python将原始边列表转换为邻接矩阵

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在图论和网络分析中&#xff0c;图是一种非常重要的数据结构&#xff0c;它由节点&#xff…

Python 中元组和列表的根本区别是什么?

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 在 Python 中&#xff0c;元组&#xff08;Tuple&#xff09;和列表&#xff08;List&#xff09;都是用于存储数据序列的数据结构&#xff0c;它们可以存储任何类型的数据&#xff0c;支持通过索引访问…

【AI开发:音频】一、GPT-SoVITS整合工具包的部署问题解决(GPU版)

前言 目前GPT-SoVITS的合成效果比较不错&#xff0c;相比较其他厂商的产品要规整的多。众多厂家中也是国内使用最多的一款了&#xff0c;并且这个整合包里携带了&#xff0c;除背景音、切割、训练、微调、合成、低成本合成等一些列完整的工具&#xff0c;也可以作为API进行使用…

StartAI智能绘图软件出现“缺少Python运行库”怎么办?

StartAI做为一款国产AI界的新秀&#xff0c;是一款贴合AIGC新手的智能绘图软件。新手安装遇见“缺少Python运行库”怎么办”&#xff1f;小编一招搞定~ 解决方法&#xff1a;手动下载【resource文件】&#xff0c;将文件添加到安装目录下。 点击链接进行手动下载噢~ 确保 Star…

React首次加载渲染2次的问题

在开发React项目的时候&#xff0c;发现useEffect会调用2次的情况&#xff0c;依赖数组明明没有变化&#xff0c;怎么会调用2次&#xff1f;百思不得其解&#xff0c;依赖没变化的话&#xff0c;那肯定是整个组件重渲染了。 最最简单的代码如下&#xff1a; const container …

【抽代复习笔记】14-群(八):变换群中的旋转、位似、平移变换群

定理1&#xff1a;集合A上的所有一一变换的集合G关于变换的乘法&#xff08;复合&#xff09;作成群。 证&#xff1a;首先&#xff0c;恒等变换ɛ:A→A(ɛ(x) x)是集合A上的一一变换&#xff0c;所以ɛ∈G&#xff0c;即G≠∅&#xff1b; ①对任意的f,g∈G&#xff0c;由映…

AI人工智能培训老师叶梓:大数据治理的关键工具:开源数据血缘分析系统

在大数据时代&#xff0c;数据的产生和传播速度日益加快&#xff0c;数据之间的关系也变得日益复杂。为了更好地管理和理解数据之间的关系&#xff0c;数据血缘分析系统应运而生。本文将介绍几个开源的数据血缘分析系统&#xff0c;它们在数据治理、数据质量管理和数据隐私保护…

ragflow 大模型RAG知识库使用案例

参考: https://github.com/infiniflow/ragflow/blob/main/README_zh.md 支持丰富的文件类型,包括 Word 文档、PPT、excel 表格、txt 文件、图片、PDF、影印件、复印件、结构化数据, 网页等。 运行步骤: 1、确保 vm.max_map_count 不小于 262144 【更多】: 如需确认 vm.…

ThingsBoard服务端使用RPC通过网关给设备发送消息

一、概述 1、发送服务器端网关RPC 二、案例&#xff1a; 1、建立设备与网关之间的通讯 2、查看设备和网关是否在线状态啊 3、通过 仪表盘&#xff0c;创建设备A的模拟RPC调用的窗口链接 4、在客户端的网关设备上订阅RPC网关的主题信息 5、通过服务端的窗口&#xff0c;发…