Compose:页面重组分析案例
前言:
刚刚开始学安卓的时候,各种xml,activtiy,以及监听的事件的繁琐调用,接触了compose大大简化了代码量,但是随着页面复杂化,出现重组次数太多导致页面卡顿感
重组和重组作用域
首先,定义一个保存重组记录的文本组件,使用SideEffect监听ref变化时更新外部状态,remember临时存储次数
class Ref(var value: Int)
@Composable
inline fun LogCompositions(msg: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Text(text = "$msg 重组次数 ${ref.value}")
Log.d("RecompositionLog", "Compositions: $msg ${ref.value}")
}
测试计数组件
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Button(onClick = {
b += 1
}) {
// Button 作用域
LogCompositions("Button")
Text(text = " $b")
// Button 作用域
}
LogCompositions("Test")
// Test 作用域
}
运行后:
D Compositions: Button 0
D Compositions: Test 0
按钮点击后,控制台Button作用域被重组
Compositions: Button 1
Button组件被重组了,为什么Test作用域中没有被重组,继续修改一下代码
@Composable
@Preview(showBackground = true)
fun Test1(){
var b by remember {
mutableIntStateOf(0)
}
Button(onClick = {
b += 1
}) {
LogCompositions("Button")
Text(text = " $b")
}
Text(text = " $b")
LogCompositions("Test")
}
运行点击如下
// 运行
D Compositions: Button 0
D Compositions: Test 0
// 点击
D Compositions: Button 1
D Compositions: Test 1
// 点击
D Compositions: Button 2
D Compositions: Test 2
修改后外部也被重组,至于修改前Test 作用域没有重组,那我们基本可以确认
- 只有读取state 的状态的组件作用域才会重组
- state 状态的重组作用域和state变量作用域无关
- 父组件重组不一定会重组子组件
如何只想重组外部Test作用域,而不重组内部,如下
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
LogCompositions("Test")
Button(onClick = {
b += 1
}) {
// Button 作用域
LogCompositions("Button")
// Button 作用域
}
Text(text = "$b")
// Test 作用域
}
运行如下
// 运行
D Compositions: Test 0
D Compositions: Button 0
// 点击
D Compositions: Test 1
至于Colum Row Box作用域,看下面的例子
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
Button(onClick = {
b += 1
}) {
// Button 作用域
LogCompositions("Button")
// Button 作用域
}
LogCompositions("Column")
Row {
Text("$b")
LogCompositions("Row")
}
}
Log.d("Test", "Test ...")
// Test 作用域
}
运行点击后
// 运行后
D Compositions: Button 0
D Compositions: Column 0
D Compositions: Row 0
D Test ...
// 点击后
D Compositions: Column 1
D Compositions: Row 1
D Test ...
在row中监听b的状态,但是Test和Column,Button中没有读取状态,所以没有重组
查看一下源码发现Colum Row Box 都是inline函数
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
在官网中,我们可以看到
Compose 如何确定重组范围?
Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state
变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来之前 Compose 会触发
recomposition,并在重组过程中执行 invalid 代码块。 Invalid 代码块即编译器找出的下次重组范围。能够被标记为
Invalid 的代码必须是非 inline 且无返回值的 @Composalbe function/lambda,必须遵循 重组范围最小化
原则。 为何是 非 inline 且无返回值(返回 Unit)? 对于 inline
函数,由于在编译期会在调用处中展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。
而对于有返回值的函数,由于返回值的变化会影响调用方,因此无法单独重组,而必须连同调用方一同参与重组,因此它不能作为入口被标记为
invalid 范围最小化原则 只有会受到 state 变化影响的代码块才会参与到重组,不依赖 state 的代码不参与重组。
来看看非内联传值的例子
简单传值,Test修改状态,Test外部和Wraper同时读取状态,同时发生重组
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
LogCompositions("Column")
Button(onClick = { b += 1 }) {
Wraper(b)
}
LogCompositions("Test : ${b}")
}
// Test 作用域
}
@Composable
fun Wraper(num: Int){
LogCompositions("Button ${num}")
}
运行
// 运行
D Compositions: Column 0
D Compositions: Button 0
D Compositions: Test : 0 0
// 点击
D Compositions: Column 1
D Compositions: Button 1 1
D Compositions: Test : 1 1
内联函数传值
Test外部不读取状态,传入内联函数Wraper,其中Button读取状态
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
LogCompositions("Column")
Wraper("$b")
}
LogCompositions("Test")
// Test 作用域
}
@Composable
fun Wraper(msg: String){
var data by remember {
mutableStateOf(msg)
}
Button(onClick = {
data = "被点击了"
}) {
LogCompositions("Button")
Text(text = " Wraper click ${data}")
}
LogCompositions("Wraper")
}
// 运行
D Compositions: Column 0
D Compositions: Button 0
D Compositions: Wraper 0
D Compositions: Test 0
// 点击
D Compositions: Button 1
// 点击
D Compositions: Button 1
从这里可以看出Test作用域并没用发生重组,通函数传值,传入的参数无法修改,也就无法重组外部作用域。所以新增了data状态,Button修改data状态,并发生了重组
如果我们想修改为,子组件修改状态并重组父组件怎么办
@Composable
@Preview(showBackground = true)
fun Test(){
// Test 作用域
val b = remember {
mutableIntStateOf(0)
}
Column {
LogCompositions("Column")
Wraper(b)
}
LogCompositions("Tes/${b.value}")
// Test 作用域
}
@Composable
fun Wraper(msg: MutableIntState){
Button(onClick = {
msg.intValue += 10
}) {
LogCompositions("Button")
}
Text(text = " Wraper click ${msg.value}")
LogCompositions("Wraper")
}
结果,子组件修改了b的状态,Test和Wraper都读取了b的value值,才发生了重组
// 运行
D Compositions: Column 0
D Compositions: Button 0
D Compositions: Wraper 0
D Compositions: Test/0 0
// 点击
D Compositions: Column 1
D Compositions: Wraper 1
D Compositions: Tes/10 1
// 点击
D Compositions: Column 2
D Compositions: Wraper 2
D Compositions: Tes/10 2
函数式传值,lambda延迟加载又会如何呢,看下面的例子
@Composable
@Preview(showBackground = true)
fun Test3(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
Button(onClick = { b += 1 }) {
Text(text = "click")
}
Wraper(msg = {b})
LogCompositions("Tes/${b}")
}
// Test 作用域
}
@Composable
fun Wraper(msg: () -> Int){
var data = msg()
Text(text = " Wraper click ${data}")
LogCompositions("Wraper")
}
结果, 虽然这采用了lambda,state发生了变化依旧重组了,后续会提到lambda优化
// 运行
D Compositions: Wraper 0
D Compositions: Tes/0 0
// 点击
D Compositions: Wraper 1
D Compositions: Tes/1 1
// 点击
D Compositions: Wraper 2
D Compositions: Tes/2 2
// 点击
D Compositions: Wraper 3
D Compositions: Tes/3 3
接下来看看,组合函数之间Lambda延迟加载,跳过传值,减少重组
下面是一个在开发中常用的例子,将外部数据结合内部数据处理
@Composable
@Preview(showBackground = true)
fun Test3(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
Button(onClick = { b += 1 }) {
Text(text = "click")
}
Wraper(msg = b)
LogCompositions("Tes/${b}")
}
// Test 作用域
}
@Composable
fun Wraper(msg: Int){
var data by remember {
mutableIntStateOf(10)
}
Button(onClick = { data += msg }) {
Text(text = " Wraper click")
}
LogCompositions("Wraper ${data}")
}
结果,当外部state b 变化后,子函数Wraper被强制重组了,其实子函数只是一个处理的过程,没必要重组
// 运行
D Compositions: Wraper 10 0
D Compositions: Tes/0 0
//click 点击
Compositions: Wraper 10 1
Compositions: Tes/1 1
// Wraper click 点击
Compositions: Wraper 11 2
将代码修改如下
@Composable
@Preview(showBackground = true)
fun Test3(){
// Test 作用域
var b by remember {
mutableIntStateOf(0)
}
Column {
Button(onClick = { b += 1 }) {
Text(text = "click")
}
Wraper(msg = {b})
LogCompositions("Tes/${b}")
}
// Test 作用域
}
@Composable
fun Wraper(msg: () -> Int ){
var data by remember {
mutableIntStateOf(10)
}
Button(onClick = { data += msg() }) {
Text(text = " Wraper click")
}
LogCompositions("Wraper ${data}")
}
结果: lambda延迟加载,不直接读取值,跳过了重组阶段
// 运行
D Compositions: Wraper 10 0
D Compositions: Tes/0 0
//click 点击
Compositions: Tes/1 1
// Wraper click 点击
Compositions: Wraper 11 2
高频访问数据,通过派生来降低次数
下面是一个定时器模拟高频操作,每10次执行一次逻辑
@Composable
@Preview(showBackground = true)
fun Test4(){
var num by remember {
mutableIntStateOf(0)
}
LaunchedEffect(key1 = Unit) {
while (true) {
delay(1.seconds)
num += 1
}
}
if (num % 10 == 0){
....
}
LogCompositions("Test4")
}
当代码中存在高频变动如定时器,轮播图,切换tab等,订阅state的函数就可能存在一直重组的情况
// 运行,每1s
D Compositions: Test4 0
D Compositions: Test4 1
D Compositions: Test4 2
D Compositions: Test4 3
D Compositions: Test4 4
D Compositions: Test4 5
通过state状态计算的结果,可以使用derivedStateOf
创建一个State对象,其State。
价值是计算的结果。
计算结果将以这样一种方式缓存,即调用State。
重复的value不会导致计算执行多次,而是读取State。
value将导致在计算期间读取的所有State对象都将在当前快照中读取,这意味着如果在观察到的上下文中(如可组合函数)读取该值,则将正确订阅派生状态对象。
没有突变策略的派生状态会在每个依赖项更改时触发更新。
为了避免更新时失效,可以通过derivedStateOf过载提供合适的SnapshotMutationPolicy。
将代码修改如下
@Composable
@Preview(showBackground = true)
fun Test4(){
var num by remember {
mutableIntStateOf(0)
}
val isPositive by remember {
derivedStateOf { num % 10 == 0 }
}
LaunchedEffect(key1 = Unit) {
while (true) {
delay(1.seconds)
num += 1
}
}
if (isPositive){
Text(text = "$num")
}
LogCompositions("Test$num")
}
结果如下,每10sTest4作用域重组一次,重组次数大幅度降低
// 运行,每10s
D Compositions: Test0 0
D Compositions: Test10 0