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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值