安卓Jetpack组成之确定重组范围并优化重组
作者:访客发布时间:2023-12-13分类:程序开发学习浏览:137
1.概述
前面的文章提到Compose的重组是智能的,Composable函数在进行重组时会尽可能的跳过不必要的重组,只对需要变化的UI进行重组。那Compose是如何认定UI需要变化呢?或者换句话说组成了是如何确定重组的范围呢。如果重组随意的发生,那么对UI的性能会是一个很不稳定的状态,时而好,时而坏。而且如果编写的UI代码有问题,那么重组将会带来状态的混乱,导致UI显示出错。所以弄清楚Compose重组的范围确定才能更好的避免重组的坑,并且可以针对具体的范围做优化,所以本文将介绍如何确定Compose重组的范围以及重组性能的优化。
2.确定可组合重组的范围
确定重组的范围有助于我们更好的理解ComposeUI的性能优化,下面我们先看一个例子:
@Composable
fun 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.d("zhongxj","范围3=>运行")
这行Log并没有打,没有打这行Log的原因需要我们了解Compose重组的底层原理:
在Compose中,经过Compose编译器处理后的Composable函数在对状态进行读取的同时,能够自动建立关联,在运行过程中,当状态变化时Compose会找到关联的代码块并将其标记为无效。在下一个渲染帧到来之前,编写会触发重组并且执行无效的代码块,而无效的代码块即为下一次重组的范围。能够被标记为无效的代码有2个两个要求,一是被标记为无效的代码必须时非内联且没有返回值的可组合函数,二是无返回值的lambda。
那么为啥参与重组的代码块必须是非Inline的无返回值函数呢?因为内联函数在编译期会在调用处展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。而有返回值的函数由于返回值会影响调用方,所以必须联通调用方一起参与重组.因此内联的有返回值的函数不能作为无效的代码块。
而了解了创作的底层重组原理,我们就可以清楚的知道了只有受到State变化影响的代码块,才会参与到重组。不依赖State的代码则不参与重组,这就是重组的最小化原则。
基于重组最小化原则,我们可以分析下我们计数器例子中的输出结果,其实看了日志发现 Log.d("zhongxj","范围3=>运行")
这行日志没有打,也就是说这行日志所在的代码块并没有参与重组,在范围2的作用域中,我们看到了这行代码Text(text = "$counter")
,很明显这行代码依赖了Counter状态,需要注意的是这行代码并不是读取Counter值的意思,它的意思是在范围2的作用域中读取Counter的值并传入Text,所以范围2是会参与重组的,日志就输出了Log.d("zhongxj","范围2=>运行")
,这时有读者可能会发现,按照重组最小化原则,那么访问Counter的最小范围应该是:范围2的作用域呀,为啥范围1的日志也会被打印呢?这里需要回想下咱们之前讲的:最小化范围的定义必须是非内联的Composable函数或者lambda。而列组件是一个内联声明的高阶函数:所以Content内部也会被展开在调用处,所以范围1和范围2就共享了重组的范围,所以输出了Log.d("zhongxj","范围1=>运行")
日志,假设将Column换成非内联的可组合,那么Log.d("zhongxj","范围1=>运行")
将不会输出,比如换成一个卡组件,读者可以自行试一下。
需要注意的是、按键虽然没有依赖计数器、但是范围2的重组会触发按键的重新调用,所以 Log.d("zhongxj","onButtonClick:点击按钮")
也会输出,但是其Content内部并没有依赖Counter,所以范围3的日志:Log.d(“zhongxj”,“范围3=>;运行”)不会输出.
补充说明: Composable 函数观察State变化并触发重组是在被称为”快照“的系统中完成的,所谓”快照“就是将被访问的状态像拍照一样保存下来,当状态变化时,通知相关的Composable应用的最新状态。”快照“有利于对状态管理进行线程隔离,在多线程场景下的重组有重要的应用
3.优化重组的性能
经过前面的分析,我没了解到了Compose的重组是智能的,遵循范围最小化原则,重组中执行到的Composable只有在其参数发生变化时,才会参与本次重组。
合成在执行后会生成一棵视图树,每个可合成对应树上的一个节点,因此Composable 智能重组的本质其实是从树上寻找对应位置的节点并与之进行比较,如果节点未发生变化则不用更新
那就是。
另外需要注意的是,视图树的实际构建过程比较复杂、Composable执行过程中,先将生成的Composure状态存入Slottable、然后框架基于Slotable生成Layout Node树,并完成最终的界面渲染。所以谨慎的说,Composable的比较逻辑是发生在Slotable中的。
3.1可组合位置索引
在重组的过程中、Composure上的节点可以完成增、删、移动、更新等多种变化、Compose编译器会根据代码调用位置,为Composable生成索引Key、并且存入Composable、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等条件语句时,会插入Start XXX Group类似的代码,并且通过添加索引Key识别节点的增减,上面的代码中会根据State的不同显示不同的Text,编译器会为if和Else分支分别建立索引,当State由True变为False时,Box发生重组,通过Key的判断可知,Else内的代码需要插入逻辑执行,而If内生成的节点需要被移除。
假设没有编译期的位置索引,而仅仅靠运行时比较,首先执行到记住(单位)时,由于缓存原因仍然会返回当前树上存放的字符串、即Call_Site_1、接着执行到Text_of_Call_Site_1、发现与当前树上的节点类型一样,参数字符串也没有变化,因此会判断为无须重组,那么文本就无法得到更新
所以,综上所述:Composable在编译期建立索引是保证其重组能够智能且正确执行的基础。这个索引是根据可组合在静态代码中的被调用位置决定的。但是在某些场景中,可组合无法通过静态代码位置进行索引,这时我们需要手动添加索引,便于在重组中进行比较
3.2通过Key添加索引信息
假设我们现在需要给一个电影列表,然后展示电影的大致信息,代码如下所示:
@Composable
fun MoviesScreen(movies:List<Movie>){
Column {
for (movie in movies){
// showMoveCardInfo 无法在编译期间进行索引,只能根据运行时的index进行索引
showMoveCardInfo(movie)
}
}
}
如上面的代码所示,基于电影的名字展示电影的信息,此时无法基于代码中的位置进行索引,只能在运行时基于索引进行索引。这样的话索引会根据Item的数量发生变化,导致无法准确进行比较。在这种情况下,当重组发生时,新插入的数据会和以前的第一个数据比较,以前的第一个数据会和第二个数据比较,然后以前的第二个数据会被当作新数据插入.结果是所有的Item都会发生重组,但是我们期望的行为是,只有新插入的数据需要重组,其他没有变化的数据不应该发生重组,所以我们可以使用Key的方法为Composable在运行时手动添加一个索引,如下所示:
@Composable
fun MoviesScreen(movies:List<Movie>){
Column {
for (movie in movies){
key(movie.id){ // 使用movie的唯一ID作为Composable的索引
showMoveCardInfo(movie)
}
}
}
}
使用电影的ID传入Composable做为唯一索引,当插入新数据时,之前对象的索引没有被打乱,仍然可以发挥比较时的锚定作用,所以其他没有发生变化的Item就可以不用参与重组
3.3使用注解@稳定的优化重组
Composable是基于参数的比较结果来决定是否重组,也就是说,只有当参与比较的参数对象是稳定的且等于返回True,才认为是相等的。Kotlin中常见的基本类型(boolean、Int、Long、Float、Char)字符串,Lambda表达式都可以认为式稳定的,因为都是不可变类型。所以他们的参数比较的结果都式可信的.但是假如参数是可变类型,那么比较的结果将是不可信的.
data class Mutabledata(var data:String)
@Composable
fun MutableDemo(){
var mutable = remember { Mutabledata("walt") }
var state by remember { mutableStateOf(false) }
if(state){
mutable.data = "zxj"
}
Button(onClick = {state = true}){
showText(mutable)
}
}
@Composable
fun ShowText(mutable:MutableData){
Text(text = mutable.data) // 会随着state的变化而变化
}
在上面的代码中,Mutable Data是一个不稳定的对象,因为它有一个Var类型的变量Data,当点击按钮改变状态时,可变会修改数据,对于Show文本来说,参数可变在状态改变前后都指向同一个对象,因此仅仅靠等于判断会认为参数没有发生变化,但实际上测试发现Show Text函数发生了重组,所以Mutable Data参数类型是不稳定的,等于结果不可信。
所以对于一些默认不被认为是稳定类型的,比如接口状态列表或者@等集合类,如果能够确保其在运行时的稳定,可以为其添加@注解,编译器会将这些类型视为稳定类型,从而发挥只能重组的作用,提升性能。代码如下所示:
@Stable
interface UiState<T>{
val value:T?
val exception:Throwable?
val hasError:Boolean
get() = exception != null
}
注意: 被添加为@Statble的普通父类、密封类、接口等其派生子类也会被认为时稳定的
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 9Android广播如何解决Sending non-protected broadcast问题
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版