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.
 *
**/
  1. equals永远不要override,因为需要保证相同的两个实例,永远return true,而不是比较内部属性。
  2. 内部public数据的变更,一定要通知所有的依赖Composiiton
  3. 所有内部的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,遵循一下几点就好:

  1. 不要使用data class,因为data class自动添加equals
  2. 数据绑定都是用mutableState
  3. 将UI关联的数据bean和业务逻辑相关的数据bean,完全拆分开。

 评论