Compose 状态机制和重组优化

  • 列表自动刷新
  • 重组优化
  • 稳定性注解:@Stable

列表自动刷新

运行下面一段程序,点击按钮,会发生什么?

class StateObserverAndAutoUpdate : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val numbers by mutableStateOf(mutableListOf(1, 2, 3))
        setContent {
            NumberList(numbers = numbers)
        }
    }
​
    @Composable
    fun NumberList(numbers: MutableList<Int>) {
        Column {
            Button(onClick = {
                numbers.add(numbers.last() + 1)
            }) {
                Text("点我:+1")
            }
            for (num in numbers) {
                Text("number: $num")
            }
        }
    }
}

运行效果图如下:

点击按钮会对numbers的最后一个数进行+1,添加到集合尾部。内容确实更新了,表面上感觉会触发绑定了numbers对象控件的刷新,实际上并没有。

这是因为add方法,不会更改numbers集合的对象,就意味不会触发numbers的赋值`setValue方法,也就不会触发mutableStateOf状态变化,继而就不会触发Compose的重组。

Compose的重组才能让NumberList方法被再次调用,完成Column列表的刷新。

所以我们对numbers集合的对象重新赋值时是不是就可以了:

class StateObserverAndAutoUpdate : ComponentActivity() {
​
    var numbers1 by mutableStateOf(mutableListOf(1, 2, 3))
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            NumberList(numbers = numbers1)
        }
    }
​
    @Composable
    fun NumberList(numbers: MutableList<Int>) {
        Column {
            Button(onClick = {
                // 更新列表对象才能触发Compose的重组
                numbers1 = numbers.toMutableList().apply {
                    add(numbers.last() + 1)
                }
            }) {
                Text("点我:+1")
            }
            for (num in numbers) {
                Text("number: $num")
            }
        }
    }
}

但是,每次都要创建一个新的集合对象去替换之前的,这也太傻了吧。为啥不能监听集合内部的元素变化触发更新呢?

其实是有办法的,这里不行的根本原因是,我们用的是mutableStateOf,它只会简单的订阅:是否赋值

如果我们用mutableStateListOf就可以实现内部元素变化,触发刷新。

class StateObserverAndAutoUpdate : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val numbers = mutableStateListOf(1, 2, 3)
        setContent {
            NumberList(numbers = numbers)
        }
    }
​
    @Composable
    fun NumberList(numbers: MutableList<Int>) {
        Column {
            Button(onClick = {
                numbers.add(numbers.last() + 1)
            }) {
                Text("点我:+1")
            }
            for (num in numbers) {
                Text("number: $num")
            }
        }
    }
}

同理,如果是Map集合,可以用mutableStateMapOf

val numberMap = mutableStateMapOf(1 to "1", 2 to "2")

重组优化

Compose的重组优化,会将Compose函数的一些不必要的更新操作跳过。解决自动更新带来更新范围过大,导致性能损耗。

setContent {
    var text by remember {mutableStateOf("未点击")}
    Column {
        Button(onClick = {
            text = "已点击"
        }) {
            Text(text = "点我更新内容")
        }
        Text(text = "内容:$text")
        println("111")
        RecomposeOptimization()
        println("333")
    }
}
​
@Composable
fun RecomposeOptimization() {
    println("222")
}

按照正常的程序运行逻辑,点击“点我更新内容”的按钮后,会打印:

I/System.out: 111
I/System.out: 222
I/System.out: 333

但是,实际运行情况是:

I/System.out: 111
I/System.out: 333

发现少了222的打印,这就是Compose做的重组优化,RecomposeOptimization函数和text文本并无关联,所以并不需要因为text内容的变化做无谓的刷新,避免这部分的性能损耗。

这是简单的字符串变化,那如果是个对象,也能做到这种程度的优化吗?比如一个user对象的变更

data class User(val name: String)
​
var clickText by mutableStateOf("点击前")
var user  = User("张三")
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Column {
            println("000")
            Button(onClick = {
              println("click update name...")
              clickText = "点击后"
              // 点击后重新赋值一个同名的新对象
              user = User("张三")
            }) {
                Text(clickText)
            }
            println("111:$clickText")
            UpdateUser(user)
            println("333")
        }
    }
}
​
@Composable
fun UpdateUser(user: User) {
    println("222 user name is: ${user.name}")
    Text(text = "name: " + user.name)
}

未点击按钮前的打印:

2022-05-01 15:54:58.915 3161-3161/com.example.playcompose I/System.out: 000
2022-05-01 15:54:58.927 3161-3161/com.example.playcompose I/System.out: 111:点击前
2022-05-01 15:54:58.928 3161-3161/com.example.playcompose I/System.out: 222 user name is: 张三
2022-05-01 15:54:58.929 3161-3161/com.example.playcompose I/System.out: 333

点击按钮后重新赋值一个同名的新对象,打印结果:

2022-05-01 16:03:05.904 3422-3422/com.example.playcompose I/System.out: 000
2022-05-01 16:03:05.925 3422-3422/com.example.playcompose I/System.out: 111:点击后
2022-05-01 16:03:05.926 3422-3422/com.example.playcompose I/System.out: 333

发现222没有打印,证明虽然对象变化了,但是由于新的对象名字没有变化,Compose重组也对引用对象的做了优化。由此可猜测背后是根据实例对象的equal方法去做对象是否“真正变化”的辨别。

有趣的是,如果把User类的val改为var

data class User(var name: String)

点击后的打印结果又会变成:

2022-05-01 16:08:50.857 3791-3791/com.example.playcompose I/System.out: 000
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 111:点击后
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 222 user name is: 张三
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 333

发现重组优化的效果又消失了。这是为什么呢?

这是因为var的可变性的特性,导致name可能会在某个时刻被修改,这是不可预期的,如果Compose此时因为去优化不去执行刷新操作,会出现不可预期的错误。所以在效率和正确性上,Compose选择了正确性,如果你使用了var,不论你对象或对象的值是否改变,就无条件刷新,不再提供重组优化。

稳定性注解:@Stable

但想想因为一个var,就无脑跳过这种重组优化,是否也太粗暴了呢?

如果我们对上述的类加上@Stable

@Stable
data class User(var name: String)

会发现,它又可以进行重组优化,跳过了不需要的更新。

@Stable它就相当于一个开关,如果开了就等于说开发者可以保证这个类的稳定,不会出现数据的意外被修改现象。

虽然是可以做到数据的稳定性,但实际上很难保证,所以不太推荐轻易使用该关键字。@Stable注解常给接口使用。

对于普通类,推荐使用以下使用参数代理的常用写法:

class User(name: String) {
    var name by mutableStateOf(name)
}

通过by mutableStateOf就可以让Compose不通过@Stable关键字,就确定该类是可靠的,它会根据name的变化,通知到绑定页面的自动刷新。

results matching ""

    No results matching ""