苏州华亭建设工程有限公司网站,网站备案有幕布,WordPress编辑,新浪体育世界杯目录 1.概述2.确定Composable重组的范围3.优化重组的性能3.1 Composable 位置索引3.2 通过Key添加索引信息3.3 使用注解Stable优化重组 1.概述
前面的文章提到Compose的重组是智能的#xff0c;Composable函数在进行重组时会尽可能的跳过不必要的重组#xff0c;只对需要变化… 目录 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,并且存入CompositionComposable在执行过程中通过与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:ListMovie){Column { for (movie in movies){// showMoveCardInfo 无法在编译期间进行索引只能根据运行时的index进行索引showMoveCardInfo(movie)}}}如上面的代码所示基于Movie的名字展示电影的信息此时无法基于代码中的位置进行索引,只能在运行时基于index进行索引。这样的话索引会根据item的数量发生变化导致无法准确进行比较。在这种情况下当重组发生时新插入的数据会和以前的第一个数据比较以前的第一个数据会和第二个数据比较然后以前的第二个数据会被当作新数据插入。结果是所有的item都会发生重组但是我们期望的行为是只有新插入的数据需要重组其他没有变化的数据不应该发生重组所以我们可以使用key的方法为Composable在运行时手动添加一个索引如下所示
Composablefun MoviesScreen(movies:ListMovie){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 UiStateT{val value:T?val exception:Throwable?val hasError:Booleanget() exception ! null
}
注意 被添加为Statble的普通父类、密封类、接口等其派生子类也会被认为时稳定的