Jetpack Compose
写起 UI
来毋庸置疑的是快。它的接口设计简单,但是涉及了很多平时不会用到的新概念,很多问题都可能是没搞懂这些概念而产生的。
今天我们就来聊聊可能因为没理解透 Recomposition
而写出的性能问题。
首先来看一段代码:
@Composable fun TestContent() { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() ) { Header(scroll = scrollState.value) Column( modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) .verticalScroll(scrollState) ) { for(i in 0 until 500){ Text(text = "scroll content") } } } } @Composable fun Header(scroll: Int) { Box( modifier = Modifier .fillMaxWidth() .height(150.dp) .background(Color.Gray) ){ Text( modifier = Modifier.align(Alignment.Center), text = "scroll = $scroll", fontSize = 17.sp ) } }
TestContent
是个 Column
, 分为上边的 Header
和下边的滚动容器, Header
会显示滚动偏移量。
我们将这段代码跑起来,可以看到结果是完全符合预期。
那是否就没有问题了呢?
这段代码是存在性能问题的:因为 Header
需要 scrollState.value
这个数据,所以我们在 TestContent
里读取 scrollState.value
,然后传递给 Header
, 而因为 scrollState.value
是一个 state
,所以它的值变化时会触发访问函数的 Recomposition
,在这里相当于框架调用了一次 TestContent
,所以每次滚动,TestContent
就会被调用数次,那个 500 次的循环也会不断地跑,这个循环每次跑出来结果是一样的,就是浪费了。如果数据再多,那么就是滚动卡顿了。
那有没有方式来发现这种问题呢?Android Studio Dolphin 的 Layout Inspector
提供了支持:
可以通过 Layout Inspector
看到在我滚动的过程中, TestContent
被 recomposed
了 18 次, 而滚动容器下都显示 skip
了 18 次,这是说循环都跑了,但是每个循环里的 Text
参数一致,所以忽略了,这也是框架已有的优化。
如果不是高版本的 Android Studio,那怎么发现这些问题呢?那只能用万能的日志大法了。
@Composable fun TestContent() { val scrollState = rememberScrollState() Log.i("test", "enter test content") Column( modifier = Modifier .fillMaxSize() ) { Header(scroll = scrollState.value) Column( modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) .verticalScroll(scrollState) ) { for(i in 0 until 500){ Log.i("test", "enter loop $i") Text(text = "scroll content") } } } }
在这两个点加上日志,我们滚动时,就可以看出源源不断的日志输出了。
那我们应该怎么优化呢?
上面已经提到,Recomposition
是函数级别的,所以我们把 scrollState.value
的访问放到 Header
里去,就不会有问题了,所以在使用 Jetpack Compose
时,很有必要去将页面划分成一个个微小的组件,使得 Recomposition
的范围尽量可控,而不要去写那种很长而且依赖的状态值很多的函数。
在例子中的情况,我们可以把 scrollState
整个传递进去,但更好的做法是官方推荐的 lambda
表达式参数传递。
// Header 传参由 Int 变为 ()-> Int @Composable fun Header(scroll: () -> Int) { Box( modifier = Modifier .fillMaxWidth() .height(150.dp) .background(Color.Gray) ){ Text( modifier = Modifier.align(Alignment.Center), text = "scroll = ${scroll()}", fontSize = 17.sp ) } } @Composable fun TestContent() { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() ) { Header(scroll = { scrollState.value }) Column( modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) .verticalScroll(scrollState) ) { for(i in 0 until 500){ Text(text = "scroll content") } } } }
这样,我们再次用 Layout Inspector
查看:
这样子被 recomposed
就只有 Header
了,如果我们用日志的方式,那么滚动时也不会有日志输出了。界面性能又提升了一丢丢。
正所谓解决复杂的问题,往往只需要简单的几行代码,最终还是要落实到原理的理解上。
最后,提个小问题来,来加深大家对 Composable
函数的理解,假设我的代码是这样子的:
@Composable fun TestContent() { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() ) { Header(scroll = scrollState.value) Content(scrollState) } } @Composable fun Header(scroll: Int) { Box( modifier = Modifier .fillMaxWidth() .height(150.dp) .background(Color.Gray) ){ Text( modifier = Modifier.align(Alignment.Center), text = "scroll = $scroll", fontSize = 17.sp ) } } @Composable fun ColumnScope.Content(scrollState: ScrollState){ Column( modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) .verticalScroll(scrollState) ) { for(i in 0 until 500){ Text(text = "scroll content") } } }
上面的代码,我只是把滚动容器抽取到一个单独的函数中,那么循环在滚动时还会不断地被执行吗? 为什么?