继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Jetpack Compose性能优化——一个视图一个状态模式让重组成更有针对性

繁星淼淼
关注TA
已关注
手记 312
粉丝 44
获赞 264

嗨,大家好,

让我们深入探讨一下 Jetpack Compose 的一章,在这章里,我们将专注于通过减少不必要的重组来优化性能。很多人分享了通过副作用、将 lambda 表达式作为参数传递等手段以及其他技巧来最小化重组的方法。

但这里有一个更厉害的地方经常被忽视——我们将探讨一视图一状态模式,这能显著减少重组。

准备好看看这怎么玩了吗?那我们就开始吧!🚀

首先,我们来看看通常是如何实现一个特性的。我会用一个简单的例子——一个带有基本UI元素的计时器来演示。

我们将创建一个活动和一个 ViewModel(这只是个例子,不一定符合最佳实践)。

    class MainActivity : ComponentActivity() {  
        @RequiresApi(Build.VERSION_CODES.O)  
        override fun onCreate(savedInstanceState: Bundle?) {  
            super.onCreate(savedInstanceState)  
            setContent {  
                val mainViewModel = hiltViewModel<MainViewModel>()  
                val state by mainViewModel.state.collectAsState()  

                LaunchedEffect(Unit) {  
                    mainViewModel.processAction(MainAction.ContinueData)  
                }  

                Data(state)  
            }  
        }  
    }  

    @Composable  
    fun Data(state: MainState) {  
        Column(  
            verticalArrangement = Arrangement.SpaceBetween,  
            horizontalAlignment = Alignment.CenterHorizontally,  
            modifier = Modifier  
                .padding(vertical = 16.dp)  
                .fillMaxSize()  
        ) {  
            ContinueUpdateText(state.text)  
            PlayerA(state.playerAText, state.listA)  
            PlayerB(state.playerBText, state.listB)  
            PlayerC(state.playerCText, state.listC)  
        }  
    }  

    @Composable  
    fun ContinueUpdateText(value: String) {  
        Text(text = value, fontSize = 12.sp, color = Color.Black)  
    }  

    @Composable  
    fun PlayerA(value: String, list: List<Int>) {  
        LaunchedEffect(value) {  
            Log.d("PlayerA", "Recomposed with value: $value, $list")  
        }  

        Text(text = value, fontSize = 12.sp, color = Color.Magenta)  
    }  

    @Composable  
    fun PlayerB(value: String, list: List<Int>) {  
        LaunchedEffect(value) {  
            Log.d("PlayerB", "Recomposed with value: $value, $list")  
        }  

        Text(text = value, fontSize = 12.sp, color = Color.Magenta)  
    }  

    @Composable  
    fun PlayerC(value: String, list: List<Int>) {  
        LaunchedEffect(value) {  
            Log.d("PlayerC", "Recomposed with value: $value, $list")  
        }  

        Text(text = value, fontSize = 12.sp, color = Color.Magenta)  
    }
    @HiltViewModel  
    class MainViewModel @Inject constructor() : BaseViewModel<MainState, MainAction>() {  

      override fun processAction(action: MainAction) {  
          when (action) {  

              MainAction.ContinueData -> {  

                  viewModelScope.launch {  
                      var counter = 0  
                      while (true) {  
                          counter = ++counter  

                          if (counter % 2 == 0) {  
                              setState(  
                                  getValue().copy(  
                                      playerAText = "A-${counter}",  
                                      listA = getValue().listA + counter  
                                  )  
                              )  
                          }  

                          if (counter % 3 == 0) {  
                              setState(  
                                  getValue().copy(  
                                      playerBText = "B-${counter}",  
                                      listB = getValue().listB + counter  
                                  )  
                              )  
                          }  

                          if (counter % 5 == 0) {  
                              setState(  
                                  getValue().copy(  
                                      playerCText = "C-${counter}",  
                                      listC = getValue().listC + counter  
                                  )  
                              )  
                          }  

                          setState(  
                              getValue().copy(  
                                  text = counter.toString()  
                              )  
                          )  

                          delay(1000)  
                      }  

                  }  

              }  

          }  
      }  

      override fun initialState(): MainState =  
          MainState("", "A-0", "B-0", "C-0", true, emptyList(), emptyList(), emptyList())  

    }  

    data class MainState(  
      val text: String,  
      val playerAText: String,  
      val playerBText: String,  
      val playerCText: String,  
      val shouldVisible: Boolean,  
      val listA: List<Int>,  
      val listB: List<Int>,  
      val listC: List<Int>  
    )  

    sealed interface MainAction {  
      data object ContinueData : MainAction  
    }

输出:

说明一下:-

  • 从这个输出中,你将会注意到重组过程正在所有组件中进行。

很多人可能认为让列表更稳定就能解决我们的问题。

那我们就试试看,让这个列表更稳定吧。

    @稳定的  
    data class ImmutableList<T>(  
        val items: List<T>  // 不可变列表  
    )

我们将使用 ImmutableList 数据类,而不是我们平常使用的 List 接口。让我们来看看这么改了之后的结果。

输出:

解释:

  • 使用 ImmutableList 确实有助于减少某些重新组合过程,但我们只有创建一个新的 ImmutableList 才能看到效果。
  • 当然,还有其他可能的场景。

这说明虽然不可变列表有所帮助,但要想有效优化还需要考虑更多因素。

这里的问题是:为什么当 PlayerB 的 composable 有变化时,PlayerA 却跳过了重组?还有,你看到了吗?Data composable 仍在重组。

为什么我们不能让它们完全分开,让Composable A完全不知道Composable B?

那不是更能解决我们重组问题的办法吗?

这就是我所说的一眼一态模式。

让我们来看看这段代码在实际操作中是怎么工作的。

    @HiltViewModel 注解
    class MainViewModel @Inject constructor() : BaseViewModel<MainState, MainAction>() {  

        override fun processAction(action: MainAction) {  
            when (action) {  

                MainAction.ContinueData -> {  
                    // 当接收到 MainAction.ContinueData 操作时,执行以下操作:
                    viewModelScope.launch {  
                        var counter = 0  
                        while (true) {  
                            counter = ++counter  

                            if (counter % 2 == 0) {  
                                setState(  
                                    getValue().copy(  
                                        playerAData = getValue().playerAData.copy(  
                                            text = "A-${counter}",  
                                            list = getValue().playerAData.list + counter  
                                        )  
                                    )  
                                )  
                            }  

                            if (counter % 3 == 0) {  
                                setState(  
                                    getValue().copy(  
                                        playerBData = getValue().playerBData.copy(  
                                            text = "B-${counter}",  
                                            list = getValue().playerBData.list + counter  
                                        )  
                                    )  
                                )  
                            }  

                            if (counter % 5 == 0) {  
                                setState(  
                                    getValue().copy(  
                                        playerCData = getValue().playerCData.copy(  
                                            text = "C-${counter}",  
                                            list = getValue().playerCData.list + counter  
                                        )  
                                    )  
                                )  
                            }  

                            setState(  
                                getValue().copy(  
                                    text = counter.toString()  
                                )  
                            )  

                            delay(1000)  
                        }  

                    }  

                }  

            }  
        }  

        override fun initialState(): MainState =  
            MainState(  
                "",  
                PlayerAData.initialData(),  
                PlayerBData.initialData(),  
                PlayerCData.initialData()  
            )  

    }  

    数据类 MainState(  
        val text: String,  
        val playerAData: PlayerAData,  
        val playerBData: PlayerBData,  
        val playerCData: PlayerCData,  
    ) {}  

    数据类 PlayerAData(  
        val text: String,  
        val list: List<Int>  
    ) {  
        伴生对象 {  
            fun initialData(): PlayerAData = PlayerAData("A-0", emptyList())  
        }  
    }  

    数据类 PlayerBData(  
        val text: String,  
        val list: List<Int>  
    ) {  
        伴生对象 {  
            fun initialData(): PlayerBData = PlayerBData("B-0", emptyList())  
        }  
    }  

    数据类 PlayerCData(  
        val text: String,  
        val list: List<Int>  
    ) {  
        伴生对象 {  
            fun initialData(): PlayerCData = PlayerCData("C-0", emptyList())  
        }  
    }  

    密封接口 MainAction {  
        数据对象 ContinueData : MainAction
    }
  • 在这里,我们为不同的可组合项创建了三个对应的数据类(data class),并相应地更新了状态。
    class MainActivity : ComponentActivity() {  
        @RequiresApi(Build.VERSION_CODES.O)  
        override fun onCreate(savedInstanceState: Bundle?) {  
            super.onCreate(savedInstanceState)  
            setContent {  
                val mainViewModel = hiltViewModel<MainViewModel>()  

                LaunchedEffect(key1 = Unit) {  
                    mainViewModel.processAction(MainAction.ContinueData)  
                }  

                Data()  
            }  
        }  
    }  

    @Composable  
    fun Data() {  
        Column(  
            verticalArrangement = Arrangement.SpaceBetween,  
            horizontalAlignment = Alignment.CenterHorizontally,  
            modifier = Modifier  
                .padding(vertical = 16.dp)  
                .fillMaxSize()  
        ) {  
            ContinueUpdateText()  
            PlayerA()  
            PlayerB()  
            PlayerC()  
        }  
    }  

    @Composable  
    fun ContinueUpdateText() {  
        val mainViewModel = hiltViewModel<MainViewModel>()  
        val text by mainViewModel.state.map { it.text }.collectAsState(initial = "")  

        Text(text = text, fontSize = 12.sp, color = Color.Black)  
    }  

    @Composable  
    fun PlayerA() {  

        val mainViewModel = hiltViewModel<MainViewModel>()  
        val playerAData by mainViewModel.state.map { it.playerAData }.collectAsState(initial = PlayerAData.initialData())  

        LaunchedEffect(key1 = playerAData.text) {  
            Log.d("PlayerA", "重新组合的值为:${playerAData.text}, ${playerAData.list}")  
        }  

        Text(text = playerAData.text, fontSize = 12.sp, color = Color.Magenta)  
    }  

    @Composable  
    fun PlayerB() {  

        val mainViewModel = hiltViewModel<MainViewModel>()  
        val playerBData by mainViewModel.state.map { it.playerBData }.collectAsState(initial = PlayerBData.initialData())  

        LaunchedEffect(key1 = playerBData.text) {  
            Log.d("PlayerB", "重新组合的值为:${playerBData.text}, ${playerBData.list}")  
        }  

        Text(text = playerBData.text, fontSize = 12.sp, color = Color.Magenta)  
    }  

    @Composable  
    fun PlayerC() {  

        val mainViewModel = hiltViewModel<MainViewModel>()  
        val playerCData by mainViewModel.state.map { it.playerCData }.collectAsState(initial = PlayerCData.initialData())  

        LaunchedEffect(key1 = playerCData.text) {  
            Log.d("PlayerC", "重新组合的值为:${playerCData.text}, ${playerCData.list}")  
        }  

        Text(text = playerCData.text, fontSize = 12.sp, color = Color.Magenta)  
    }

输出:

解释:

  • 单个重组:每个可组合项会根据其状态更新独立重组。这确保了一个可组合项的变化不会触发其他可组合项不必要的重组。
  • 无需跳过重组:采用这种方法,就不需要跳过重组了。每个可组合项高效地管理自己的状态。
  • 父可组合项稳定性:父可组合项(Data)在子可组合项发生变化时不会进行重组。每个子可组合项都是隔离的。
  • 状态映射:我们利用map运算符为每个可组合项映射状态。这创建了一个新的流,只观察每个可组合项相关的状态部分。
  • 高效观察:通过为每个可组合项提供一个新的流,我们确保只观察必要的状态变化,从而提高性能并减少不必要的更新。

如果有任何问题,可以在评论里留言,我会尽快回复你,ASAP。我们很快就会深入探讨 Jetpack Compose。

祝你编程开心,直到那时!

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP