关于Jetpack Compose重绘(Recomposition)的一个坑

在这里插入图片描述
最近尝鲜Jetpack Compose时,踩了一个坑,可能是很多人都容易忽略的问题,特此记录一下。
当前最新版本: 1.0.0-alpha11

Compose重绘


关于Compose的重绘(Recomposition),官方是这样介绍的:

Recomposition skips as much as possible
When portions of your UI are invalid, Compose does its best to recompose just the portions that need to be updated. This means it may skip to re-run a single Button’s composable without executing any of the composables above or below it in the UI tree.

下面是官方配套的例子:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    
    
    Column {
    
    
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumnFor is the Compose version of a RecyclerView.
        // The lambda passed is similar to a RecyclerView.ViewHolder.
        LazyColumnFor(names) {
    
     name ->
            // When an item's [name] updates, the adapter for that item
            // will recompose. This will not recompose when [header] changes
            NamePickerItem(name, onNameClicked)
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    
    
    Text(name, Modifier.clickable(onClick = {
    
     onClicked(name) }))
}

根据官方文档的介绍,当Composable函数签名中的参数不发生变化时,不应重绘,从而提高整体重绘性能。但经过测试这仅限于调用外部Composable函数的时候,对于内部直接调用的DSL,则无法通过参数判断是否参与重绘:

如下:

@Composable
fun ParentComponent(
	list: List<Data>
) {
    
    
	Log.d("compose", "render parent")
	ChildComponent(list) 
}

@Composable
fun ChildComponent(list :List<Data>) {
    
    
	Box{
    
    
		Log.d("compose", "render child")
	}
}

由于ChildComponent签名中依赖了list,当list变化引起重绘时的日志如下

D/compose: render parent
D/compose: render child

如果ChildComponent改为无参函数:

@Composable
fun ParentComponent(
	list: List<Data>
) {
    
    
	Log.d("compose", "render parent")
	ChildComponent() 
}

@Composable
fun ChildComponent() {
    
    
	Box{
    
    
		Log.d("compose", "render child")
	}
}
D/compose: render parent

由于对list不再依赖,当list变化引起重绘时child不再重绘

内联组件的重绘


当我们把child直接内联到parent中时:

@Composable
fun ParentComponent(
	list: List<Data>
) {
    
    
	Log.d("compose", "render parent")
	Box{
    
    
		Log.d("compose", "render child")
	}
}
D/compose: render parent
D/compose: render child

虽然Box{ ... }内部没有对list的依赖,但是仍然参与了重绘。

扫描二维码关注公众号,回复: 12648738 查看本文章

按照正常的思考,一段逻辑,无论是否抽成独立的函数,应该不影响其执行时的行为。但是对于Composable函数,这里却反常识的。

原因猜想


试着猜想一下原因:

我们知道Composable函数在编译期会生成很多逻辑代码,包括是否参与重绘的检查逻辑:仅当参数发生变化时重新执行Composable。

但是对于内联的情况,因为闭包可以随意访问外部成员,所以无法通过参数简单的判断,这会增加编译期的工作量,所以目前DSL的尾lambda中无论是否引用了变化的数据都会参与重绘。

为了验证一下猜想,我们把提出的函数加上inline试了一下:

@Composable
fun ParentComponent(
	list: List<Data>
) {
    
    
	Log.d("compose", "render parent")
	ChildComponent() 
}

@Composable
inline fun ChildComponent() {
    
    
	Box{
    
    
		Log.d("compose", "render child")
	}
}
D/compose: render parent
D/compose: render child

果不其然,添加inline后,即使没有参数依赖仍然参与重绘

最后


这个不知道算不算BUG,但肯定是需要开发者注意的地方,不要想当然的判断某些子组件或许不参与重绘,所以在实现里加载了一些私货。任何Composable函数都要以纯函数的标准去实现,无论是否多次重绘都不影响其行为。

当然也希望官方能在未来的版本中能够对其优化,无论是否是内联组件,行为能够保持一致,避免为开发者带来困惑。

猜你喜欢

转载自blog.csdn.net/vitaviva/article/details/113806838