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.mfbz.cn/a/567636.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Docker镜像与容器操作

一、Docker 镜像操作 1.1 搜索镜像 格式&#xff1a;docker search 关键字 docker search nginx 1.2 获取镜像nginx 格式&#xff1a;docker pull 仓库名称[:标签] 如果下载镜像时不指定标签&#xff0c;则默认会下载仓库中最新版本的镜像&#xff0c;即选择标签为 latest…

SwiftUI 5.0(iOS 17.0)触摸反馈“震荡波”与触发器模式趣谈

概览 要想创作出一款精彩绝伦的 App&#xff0c;绚丽的界面和灵动的动画并不是唯一吸引用户的要素。有时我们还希望让用户真切的感受到操作引发的触觉反馈&#xff0c;直击使用者的灵魂。 所幸的是新版 SwiftUI 原生提供了实现触觉震动反馈的机制。在介绍它之后我们还将进一步…

HBase的简单学习三

一 过滤器 1.1相关概念 1.过滤器可以根据列族、列、版本等更多的条件来对数据进行过滤&#xff0c; 基于 HBase 本身提供的三维有序&#xff08;行键&#xff0c;列&#xff0c;版本有序&#xff09;&#xff0c;这些过滤器可以高效地完成查询过滤的任务&#xff0c;带有过滤…

Redis中的缓存击穿、缓存穿透、缓存雪崩问题

1.什么是缓存击穿&#xff1f; 客户端恶意访问一个不存在的数据&#xff0c;从而造成穿透缓存&#xff0c;请求直接到达数据库&#xff0c;频繁的发送这一类的请求&#xff0c;直接查询数据库&#xff0c;数据库的压力变大。 1.1如何解决缓存击穿呢&#xff1f; 1&#xff0…

基于harris角点和RANSAC算法的图像拼接matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ....................................................................... I1_harris fu…

【MySQL]】数据库操作指南之数据库的基础操作

&#x1f331;博客主页&#xff1a;青竹雾色间 &#x1f331;系列专栏&#xff1a;MySQL探险日记 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 ✨人生如寄&#xff0c;多忧何为 ✨ 文章目录 1. 创建数据库2.数据库的编码集与校验集2.1 编码集 (Character…

嵌入式Python基础1-2

嵌入式Python基础1-2 条件语句 if elif else 随机数random eval while循环 for循环 水仙花数 循环else list 列表常用方法 增删改查 加排序 append remove pop index() 升序sort(&#xff09;降序 sort(reverseTrue) 反转 reverse&#xff08;&#xff09;…

ESP32开发

1、简介 1.1 种类 WIFI模块在PC上做为客户端、服务器&#xff0c;在STM32上做服务器的通讯。在物联网应用开发有重要作用&#xff0c;种类居多&#xff0c;如下图 红色方框的esp8266-01s型号的无限wifi模块就是本章学习的主要对象。 1.2 特点 小巧的尺寸&#xff1a;ESP-01…

SpanBert学习

SpanBERT: Improving Pre-training by Representing and Predicting Spans 核心点 提出了更好的 Span Mask 方案&#xff0c;也再次展示了随机遮盖连续一段字要比随机遮盖掉分散字好&#xff1b;通过加入 Span Boundary Objective (SBO) 训练目标&#xff0c;增强了 BERT 的性…

python自动生成SQL语句自动化

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python自动生成SQL语句自动化 在数据处理和管理中&#xff0c;SQL&#xff08;Structured …

WAF防范原理

目录 一、什么是WAF 二、纵深安全防御 WAF的组网模式 WAF配置全景 WAF端 服务器 攻击端 拦截SQL注入&#xff0c;XSS攻击&#xff0c;木马文件上传 要求&#xff1a; 使用WAF&#xff0c;通过配置策略要求能防御常见的web漏洞攻击&#xff08;要求至少能够防御SQL、XSS、文…

毕业设计注意事项

1.开题 根据学院发的开题报告模板完成&#xff0c;其中大纲部分可参考资料 2.毕设 根据资料中的毕设评价标准&#xff0c;对照工作量 3.论文 3.1 格式问题 非常重要&#xff0c;认真对比资料中我发的模板&#xff0c;格式有问题&#xff0c;答辩输一半&#xff01; 以word…

wireshark RTP分析参数

主要看丢弃和Delta&#xff0c; 丢弃就是丢掉的udp包&#xff0c;所占的比率 Delta是当前udp包接收到的时间减去上一个udp包接收到的时间 根据载荷可以知道正确的delta应该是多少&#xff0c;比如G711A&#xff0c;ptime20&#xff0c;那么delta理论上应该趋近于20. 这里的de…

C++面向对象程序设计 - 运算符重载

函数重载就是对一个已有的函数赋予新的含义&#xff0c;使之实现新的功能。因此一个函数名就可以用来代表不同功能的函数&#xff0c;也就是一名多用。运算符也可以重载&#xff0c;即运算符重载&#xff08;operator overloading&#xff09;。 一、运算符重载的方法 运算符重…

# IDEA2019 如何打开 Run Dashboard 运行仪表面板

IDEA2019 如何打开 Run Dashboard 运行仪表面板 段子手168 1、依次点击 IDEA 上面工具栏 —> 【View】 视图。 —> 【Tool Windows】 工具。 —> 【Run Dashboard】 运行仪表面板。 2、如果 【Tool Windows 】工具包 没有 【Run Dashboard】 运行仪表面板 项 依次…

【好书推荐7】《机器学习平台架构实战》

【好书推荐7】《机器学习平台架构实战》 写在最前面《机器学习平台架构实战》编辑推荐内容简介作者简介目  录前  言本书读者内容介绍充分利用本书下载示例代码文件下载彩色图像本书约定 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&…

STM32系统参数和结构

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. 基本参数 2. 片上资源&#xff08;外设&#xff09; 3. STM32系列命名规则 4. 系统结构 5. 引脚定义 6. 启动配置 7. 最小系统电路 8. 型号分类和缩写 1. 基本参数 STM32F103C8T6 系列&#…

达梦(DM)数据库表索引

达梦DM数据库表索引 表索引索引准则其他准则 创建索引显式地创建索引其他创建索引语句 使用索引重建索引删除索引 表索引 达梦数据库表索引相关内容比较多&#xff0c;常用的可能也就固定的一些&#xff0c;这里主要说一下常用的索引&#xff0c;从物理存储角度进行分类&#…

B008-方法参数传递可变参数工具类

目录 方法参数传递可变参数冒泡排序Arrays工具类Arrays工具类常用方法 方法参数传递 /*** java中只有值传递* 基本数据类型 传递的是具体的值* 引用数据类型 传递的是地址值*/ public class _01_ParamPass {public static void main(String[] args) {// 调用方法 getSumge…

网络变压器在网络分析仪上能通过测试,装上设备后网速达不到呢?

Hqst华轩盛(石门盈盛)电子导读&#xff1a;今天和大家一起探讨网络变压器在网络分析仪上能通过测试&#xff0c;装上设备后网通设备网速达不到的可能原因及其处理方式 一、出现这种情况可能有以下原因&#xff1a; 1.1. 设备兼容性问题&#xff1a;设备其它元器件与 网络…
最新文章