1.概述
我们都知道,在传统的View中,若要改变UI,需要我们修改View的私有属性,比如要修改一个TextView的文字,我们需要通过它的setText("xxx")方法去修改。而Compose 则是通过重组来刷新UI。在之前的状态管理的文章中也提到过重组的概念。本章主要就是介绍Compose的重组和刷新相关的内容
2.Compose智能重组
compose的重组是很智能的,当重组发生的时候,只有状态发生改变的Composable函数才会参与重组,没有变化的Composable则会跳过本次重组。例如在计数器的demo中:
kotlin
class ComposeCounterAct : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyComposeTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
CounterComponent()
}
}
}
}
@Composable
fun CounterComponent() {
Column(modifier = Modifier.padding(16.dp)) {
var counter by remember { mutableStateOf(0) }
Text(
"$counter",
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Row {
Button(
onClick = { counter-- },
modifier = Modifier.weight(1f)
) {
Text("-")
}
Spacer(Modifier.width(16.dp))
Button(
onClick = { counter++ },
modifier = Modifier.weight(1f)
) {
Text("+")
}
}
}
}
}
当点击Button按钮的时候,counter的状态变化会触发整个Coloum范围的重组。重组过程中显示计数器值的文字组件会被赋予新的counter值,以显示更新后的数字。但是如果此时有另外一个不依赖counter状态的文字组件,则它是不参与重组的,因为Compose编译器会在编译期间插入相关的比较代码,这些比较代码会让没有依赖变更的状态的组件,在对应状态更新时,不参与重组。
有读者可能会疑惑,在Button的onclick方法中也依赖了counter,那它会发生重组吗?答案是不会,因为重组只会在Composable函数中进行,而onClick并非是一个Composable函数,所以和重组无关。此外,Button控件也没有依赖counter状态,所以也不会参与重组
3.避免掉入重组的"坑"
在前面的文章中我们提到过,Composable在编译期间代码会发生变化,所以代码的实际运行情况可能并不如我们预期的那样。所以我们需要了解下Composable在重组执行时的一些特性,避免掉入重组的"坑"。所以我们需要理解掌握下面的内容:
3.1 Composable函数的执行顺序不固定
当我们的代码中出现多个Composable函数时,他们并不一定会按照代码中出现的顺序执行,比如在Navigation中处于Stack最上方的UI会优先被绘制,在一个Box布局中处于前景的UI会具有较高的优先级,因此Composable函数会根据优先级执行。例如:
kotlin
@Composable
fun ButtonRow(){
NavigationBar {
StartScreen()
MiddleScreen()
EndScreen()
}
}
如上面的代码所示,在代码中ButtonRow函数一次调用了 StartScreen()、MiddleScreen()、 EndScreen()三个方法,,我们不能预设这三个方法一定是按照顺序执行的。也不要试图去在StartScreen中添加一个全局变量,然后在MiddleScreen()中获取到这个变化,这种关联被称作副作用,在Web前端开发的Vue中也有这个概念。副作用会让我们的UI状态混乱,而且不易维护,所以我们编写Compose时应该尽量避免副作用。
3.2 Composable 会并发执行
Composable在进行重组时不一定执行在主线程中,他们可能在后台线程池中并行执行,这样有利于发挥多核处理器的性能优势,但是由于多个composable在同一时间执行在不同线程,此时必须要注意考虑线程安全的问题。如下面例子所示来自于《Jetpack Compose 实战》一书:
kotlin
@Composable
fun EventsFeed(localEvents:List<Event>,nationalEvent:List<Event>){
var totalEvents = 0
Row{
Column { // 注释1
localEvents.forEach {
event -> Text("Item: ${event.name}")
totalEvents ++
}
}
Spacer(modifier = Modifier.height(10.dp))
Column { // 注释2
nationalEvent.forEach {
event -> Text("Item: ${event.name}")
totalEvents ++
}
}
Text(
if(totalEvents == 0) "No Events." else
"Total events $totalEvents"
)
}
}
在上面的例子中想通过totalEvents记录events的数量并在Text上显示,但是注释1和注释2 处的Column代码有可能在不同的线程中并行执行,所以就导致了totalEvents的累加是非线程安全的,结果可能是不正确的。即使totalEvents的结果正确,但是由于Text可能运行在单独线程,所以也不一定能显示正确的结果,所以这里还是副作用带来的问题,需要我们避开。
3.3 Composable会反复的执行
除了重组会造成Composable的再次执行外,在动画等场景中每一帧的变化都可能引起Composable的执行。所以Composable在短时间内可能会反复的执行,而且我们无法准确的判断它的执行次数。因此我们必须考虑到:即使多次执行也不应该出现性能问题,更不能对外部产生额外有害的影响。同样。借用书中的例子:
kotlin
@Composable
fun EventsFeed(netWorkService: EventsNetWorkService) {
val events = netWorkService.loadAllEvents()
LazyColumn {
items(events) { event ->
{
Text(text = event.name)
}
}
}
}
如上面代码所示,在方法EventsFeed中,loadAllEvents是一个IO操作,是一个耗时操作,执行的成本高如果在Composable中同步调用,那么在重组时会造成严重的卡顿问题。所以我们编写代码的时候需要注意Composable会反复的执行,编写UI的时候需要时刻注意这一点
3.4.Composable的执行时"乐观"的
所谓的"乐观"是指Composable最终总会依据最新的状态正确地完成重组。某些场景下,UI状态可能会连续的发生变化,这可能会导致中间态的重组在执行中被打断,新的重组会插入进来。对于被打断的重组,Composable不会将执行一般的重组结果反馈到视图树上,因为它 知道最后一次状态总归是正确的,所以中间的状态丢失了也不影响。
4 总结
本节介绍了Compose的智能重组和刷新,以及重组过程中可能会掉入的"坑",这些坑我们需要去属性它,因为Compose框架要求Composable作为一个无副作用的纯函数运行,我们只要在开发的过程中遵循这一原则,那么重组中的"坑"就不会再是坑,而是我们提高程序执行性能的有效方法。