Recompose和Stable
重组作用域影响范围和智能优化:
影响范围
- 重组作用域的影响范围,不只是自己的子节点,以及自己同级别的节点,也会被Recompose
setContent{
Column{
Button("Add",Modifier.clickable{
list.add(list.last() + 1)
})//一旦点击Add,
Text(item,Modifiter.clickable{
item += 1
})
for(i in list){
Text("value is $i")
}
}
}
Recompose执行过程
- 大家可以看看Column、Layout、ReusableComposeNode等Compose代码,他们都是Kotlin中的inline函数。
- 也就是说编译完成之后的class中,是没有我们编写的Column和Layout等这些代码块的。
- Compose编译的完成的组合,其实是将我们大部分的代码都拍平。
- 也正因为这个,所以才出现上面的不只是子节点会Recompose 。
val list by mutableStateListOf(1,2,3,4)
val item by mutableStateOf(1)
setContent{
Log.i(TAG,"I am recompose setContent.")
Column{
Log.i(TAG,"I am recompose column.")
Text("Rock'N'Roll")//当这个Text()出现Recompose的时候,外面的几个节点所囊括的数据也会执行Recompose
}
Text("text")
}
- 这里还有一个细节,我们会发现范围影响比较大的时候,如果我们在Compose树中有过多的log输出,也是稍有影响的。
智能优化
- 但是Recompose范围虽然会影响范围大,但是并不是一定会做影响真实性能的“Recompose”。
- 换句话说,Compose内部在编译的过程中,确实会做一些智能优化的事情,来避免频繁的UI重绘制而影响性能。
- 当然这是个先有鸡还是先有蛋的问题。并不是这种智能优化比传统的UI编写方式更好。
- 通常命令式UI能做到精准控制我们需要更新的每一个UI细节,这天然是能够达到性能优化的前提的。
- 但是声明式UI本身并不具备这个能力,所以才需要智能优化来做这些事情。因此并不是智能优化能够比精准控制做得更好,这可能在未来会更好,但并不是智能优化出现的初衷。
- 智能优化是为了解决声明式UI本身的刷新控制不足,而出现的。
val list by mutableStateListOf(1,2,3,4)
val item by mutableStateOf(1)
setContent{
Log.i(TAG,"I am recompose setContent.")
Column{
Log.i(TAG,"I am recompose column.")
Text("Rock'N'Roll")//当这个Text()出现Recompose的时候,外面的几个节点所囊括的数据也会执行Recompose
}
Text("text")
Rock()
}
@Composable
fun Rock(){
Log.i(TAG,"I am inside Rock")//这里的代码,在Recompose的时候,不会重复执行。因为从编译过程来看,Compose知道,它们是Stable的
Text("Rock")
}
比对来源
- 我们知道智能优化肯定是需要根据对象是否变化,来决定对象所依赖的UI是否需要进行刷新。
- 这种比对的来源,到底是地址比对,还是对象比对呢?答案肯定是不言而喻的。
var user =
User()
var time = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SettingContent()
}
}
@Composable
private fun SettingContent() {
var loginText by remember {
mutableStateOf("立即登陆")
}
Column {
Modifier
.background(Color.White)
.height(IntrinsicSize.Max)
Box(modifier = Modifier.size(100.dp, 100.dp))
Text(
loginText,
Modifier
.background(
Color(0x1A1E293D),
RoundedCornerShape(20.dp)
)
.clickable {
time++
loginText += "$time"
user = User()
}
.padding(90.dp, 40.dp),
fontSize = 36.sp,
color = Color(0xCC1E293D),
textAlign = TextAlign.Center,
)
Box(Modifier.size(100.dp, 100.dp))
CommonLogUtils.i(ComposeSettingActivity.TAG, "recompose value is $user")
UserNow(user)
}
}
@Composable
fun UserNow(user: User) {
CommonLogUtils.i(ComposeSettingActivity.TAG, "rock with user now refresh")
Text(user.id, fontSize = 100.sp)
}
class User(val id: String = "id", val name: String = "user") {
override fun toString(): String {
return "User(this ${System.identityHashCode(this)} id='$id')"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}
- 如上代码,我们很多时候,使用一个对象,判断equals,一般比较它的id是否变更,来决定对象是否变更。
- 比如我们在代码里,每一次点击,都将user更新为一个新的对象,都并不会真正地触发UserNow()的重新绘制。
例外和UnStable
- 上面的比对,固然是因为equals,但是不妨把上述代码的User类,修改成如下:
class User(var id: String = "id", var name: String = "user") {//将val修饰,改为var
override fun toString(): String {
return "User(this ${System.identityHashCode(this)} id='$id')"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
- 将User的属性,变成可以更改的var,我们再运行代码,一定会发现,equals失效了,user对象变更,就会触发UserNow()的UI刷新。
- 这是因为,只要User的属性只要可以变更,Compose就会认为我们是用了一个UnStable的类,equals比对就会跳过,每次比较都直接刷新UI。
为什么不可靠
- 因为Compose会认为,user是一个可以被改变对象的数据,但是同时User中的数据也是可变的。
- 那在UserNow()中,它所监听的user,有可能是一直指向同一个Object,此时如果真实被研发所制定的user,变成了另一个Object的时候,user内部属性的变更,Compose就监听不到了。
- 毕竟var user = User() 声明出来的user对象,其实是可以在任意地方被赋值改变的,这种改变一定有可能会监听不到。
- 因此,这会导致UI的刷新是不可靠的
- 为了这种UI刷新一定是可靠的,设计之初就必须抛弃一定的性能。
自行解决可靠
- 上面聊到的不可靠的问题,其实在一定程度上,我们可以通过编程约束来避免。甚至说我们可以为了性能,现行抛弃可靠性问题。
- 当出现了可靠性问题的时候,我们再去解决就好了。
- 毕竟一个框架的设计,需要提供给开发者选择,我来解决可靠性,还是我要性能优先。
Stable注解
- 见名知意,这个注解就是告诉Compose,它修饰的对象,是可靠的。每次进行Recompose的比对时,直接进行equals比较就好,不需要一刀切地刷新UI。
要求
既然要使用Stable,我们就需要保证可靠性。有三个条件要遵守
/**
* When applied to a class or an interface, [Stable] indicates that the following must be true:
*
* 1) The result of [equals] will always return the same result for the same two instances.
* 2) When a public property of the type changes, composition will be notified.
* 3) All public property types are stable.
*
**/
- equals永远不要override,因为需要保证相同的两个实例,永远return true,而不是比较内部属性。
- 内部public数据的变更,一定要通知所有的依赖Composiiton
- 所有内部的public数据,都一定要保证Stable
外部对象比较内存地址
- 不要overide equals。它们永远依据内存做同一对象比较就好。
内部数据保证变更通知Composition
- 这个也简单,User对象内部的数据变更,通知所有Composition,Compose有标准的API提供的!
- 那就是大名鼎鼎的 MutableState
@Stable
class User(id:String,name:String){
val id by mutableStateOf(id)
val name by mutableStateOf(name)
}
内部public数据都保证Stable
- 这个就是一个递归问题。User内部的所有其他对象,也得保证自己内部的次级属性,都是mutableState就好了
User(name:String,home:Home){
val name by mutableStateOf(name)
val home by mutableStateOf(home)
}
Home(addr:String){
val addr by mutableStateOf(addr)
}
总结:
在我们Compose编程过程中,使用到的关联UI Recompose的数据Bean,遵循一下几点就好:
- 不要使用data class,因为data class自动添加equals
- 数据绑定都是用mutableState
- 将UI关联的数据bean和业务逻辑相关的数据bean,完全拆分开。