前言

本文把Jetpack Compose简称为Compose,在开端之前,先清晰几个重要的概念。

  • @Composable注解标示的函数或者Lambda,称为可组合项。
  • 由N个可组合项组成的树状结构,称为组合。
  • 第一次烘托的组合,称为初始组合。
  • 初始组合之后,从头烘托可组合项,称为重组。
  • 一次完好的重组包括履行并烘托可组合项,假如只履行未烘托,称为越过重组。

本文重视的是烘托UI的可组合项,Compose中还有一些不烘托UI的可组合项,不在本文讨论规模内。

重组规模

重组规模是指,重组时,从哪个可组合项开端重组,了解重组规模很重要,举个比如:

@Composable
private fun Content() {
    var number by remember { mutableStateOf(0) }
    Log.i("compose-demo", "execute content")
    Column {
        Text(
            text = number.toString(),
            modifier = Modifier
                .clickable {
                    // 点击修正number,触发重组
                    Log.i("compose-demo", "click")
                    number++
                }
                .padding(10.dp)
        ).also {
            Log.i("compose-demo", "execute text")
        }
    }
}

代码很简单,Text显现点击的次数number,点击Text修正number值,number实践上是一个MutableState,通过by托付就好像一个一般的Int相同,能够直接读写。

在Compose中对State的修正,会触发读取State的可组合项产生重组。所以修正number会触发重组,顺便提一下,MutableState继承了State

运转代码并点击Text,看日志输出:

compose-demo             I  click
compose-demo             I  execute content
compose-demo             I  execute text

能够看到,ContentText,这2个可组合项都被履行了。Compose是这样确认重组规模的:

  1. 查找读取State的可组合项
  2. 查找第1步中可组合项的父可组合项
  3. 调用父可组合项开端重组

父可组合项是指,离当时可组合项最近的上级可组合项,下面,咱们把父可组合项简称为父项

在咱们的比如中,第1步查找到了Text这个可组合项,第2步查找到了Content这个父项,第3步履行Content开端重组,终究Text产生了重组。

这儿你可能会有疑问,Text的父项为什么是Content,而不是Column lambda,由于Column是一个inline的函数,它的lambdainline了,所以找到的是Content

Layout Inspector看看Text的重组:

Jetpack Compose - 关于重组

两列红框,左面一列显现重组的次数,右边一列显现越过重组的次数。能够看到点击后,Text产生了1次重组。

假如重组规模内有多个可组合项,抱负状况下,参数不变的可组合项应该越过重组。

修正代码,新增可组合项,测验一下越过重组:

@Composable
private fun Content() {
    var number by remember { mutableStateOf(0) }
    Log.i("compose-demo", "execute content")
    Column {
        Text(
            text = number.toString(),
            modifier = Modifier
                .clickable {
                    Log.i("compose-demo", "click")
                    number++
                }
                .padding(10.dp)
        ).also {
            Log.i("compose-demo", "execute text")
        }
        // 新增一个Text
        Text(text = "new text").also {
            Log.i("compose-demo", "execute new text")
        }
    }
}

代码只新增了一个Text,它的参数是固定的字符串new text

运转代码,并点击上面的Text,检查重组状况:

Jetpack Compose - 关于重组

从日志看,新增的Text在重组时被履行了,由于它在重组规模之内。留意,它只是被履行了,并没有从头烘托,在Layout Inspector中能够看到,它越过了1次重组。

越过重组

可是,并不是一切参数不变的可组合项都会越过重组,举个比如:

@Composable
private fun Content() {
    var number by remember { mutableStateOf(0) }
    // 用户信息
    val user = remember { User() }
    Column {
        Text(
            text = number.toString(),
            modifier = Modifier
                .clickable {
                    number++
                }
                .padding(10.dp)
        )
        // 用户信息
        UserInfo(user = user)
    }
}
@Composable
private fun UserInfo(user: User) {
    Text(text = user.name)
}
class User(
    var name: String = "default"
)

代码比较简单,创立一个UserInfo函数,它的参数类型是User,用来显现用户信息。把UserInfo放在Text的下面,而且给它传一个user参数,user参数始终不变,是同一个目标。

运转代码,看看重组状况:

能够看到,尽管参数user不变,但UserInfo仍是重组了,而不是越过重组。这是为什么呢?由于Compose以为UserInfo的参数userunstable,即不稳定的状态。

  • 当可组合项有unstable参数,它不会越过重组
  • 当可组合项的一切参数都是stable,即稳定的,它才有时机越过重组

什么状况下,Compose会以为可组合项的某个参数是unstable?要点来了:

当可组合项的参数内容能够被修正,而且Compose不能确认参数内容是否被修正了,Compose就会把这个参数标记为unstable

在咱们的比如中,User类的name特点是var的字符串,能够在任何地方被修正,而且Compose不知道什么时分会被修正,所以user参数是unstable

假如参数被修正了,Compose又不知道参数被修正了,那Compose就不会及时烘托修正后的参数,导致数据变了,UI没及时变的bug。

所以遇到含有unstable参数的可组合项,Compose会做一下最终的挣扎,只要在重组规模内,每次重组都烘托。尽管没办法确保数据变了UI及时变,但至少烘托后是正确的。

参数不变的时分,这种多余的重组,会影响功能,所以要尽量让可组合项的参数stable,这样子可组合项才有时机越过重组,进步功能。

咱们修正代码,尝试让参数user变为stable

class User(
    val name: String = "default"
)

name特点由var改为val,不答应修正。运转代码,看看重组状况:

Jetpack Compose - 关于重组

能够看到UserInfo现已能够越过重组了,此刻参数user现已是stable了。

为什么改为val就能够了呢?由于此刻User目标一旦被创立就无法修正了,也便是说UserInfo烘托之后,要改变它,只能传一个新的User目标给它,这样就做到了数据改变,UI也及时改变。

假如可组合项的一切参数都是stable的,在重组产生时,Compose会用上一次烘托UI的参数和本次的参数逐个比较。

怎么比较参数呢?

先比较是否同一个目标,假如是同一个目标,持续比较下一个参数,否则调用equals比较,回来true持续比较下一个参数,回来false,中止比较,开端重组。

现在User是一个一般类,没有重写equals,所以默认equals比较的是目标的引证。这样会导致两个不同目标他们的name值相同,也会产生重组。假如两个不同目标的name值相同,应该要越过重组。

再优化一下代码:

class User(
    val name: String = "default"
) {
    override fun equals(other: Any?): Boolean {
        Log.i("compose-demo", "$this equals")
        return if (other is User) {
            this.name == other.name
        } else {
            false
        }
    }
    init {
        Log.i("compose-demo", "$this init")
    }
}

重写了equals,并打印日志,此刻即使两个不同的目标,只要他们的name值相同,就能够越过重组。

留意:实践开发中应该一起重写hashCode,以使目标能够在哈希算法的容器中被正确运用。

@Composable
private fun Content() {
    var number by remember { mutableStateOf(0) }
    // number作为key
    val user = remember(number) { User() }
    Column {
        Text(
            text = number.toString(),
            modifier = Modifier
                .clickable {
                    number++
                }
                .padding(10.dp)
        )
        UserInfo(user = user)
    }
}

修正代码,把number当作remember的key,当number改变时,会创立一个新的User目标赋值给user参数,可是目标的name值不变,都是default

测验一下目标改变,特点值不变的状况下,是否会越过重组。

Jetpack Compose - 关于重组

从日志能够看出,第一次创立的目标是User@d372b4c,点击之后创立了新目标,而且User@d372b4c目标的equals被调用了,由于他们的name值相同,所以equals回来true,越过了重组。

实践开发中,为了方便,咱们能够直接运用data class

stable vs unstable

字符串,基本数据类型以及Lambda被默以为stable,由于他们一旦被创立就无法修正。

Compose编译器支撑输出日志,让咱们能够直观的看到参数是stable仍是unstable

修正build.gradle.kts,添加以下装备:

// build.gradle.kts
kotlinOptions {
    freeCompilerArgs += listOf(
        "-P",
        "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler"
    )
}

从头运转代码,在装备的目录下会生成日志文件:

Jetpack Compose - 关于重组

能够看到UserInfo函数被标记为skippable,表示支撑越过重组,一起它的参数user被标记为stable,由于User类被标记为stable

咱们把Username改回var,看看输出的结果:

class User(
    var name: String = "default"
)

运转代码,检查输出结果:

Jetpack Compose - 关于重组

能够看到又变回unstable了。

unstable类型

在实践开发中,比较常见的unstable类型是一些调集接口,例如ListSetMap

修正一下代码:

@Composable
private fun Content() {
    var number by remember { mutableStateOf(0) }
    val user = remember { User() }
    Column {
        Text(
            text = number.toString(),
            modifier = Modifier
                .clickable {
                    number++
                }
                .padding(10.dp)
        )
        UserInfo(user = user)
    }
}
@Composable
private fun UserInfo(user: User) {
    Text(text = user.cars.toString())
}
data class User(
    val cars: List<String> = listOf("BMW"),
)

User类现在只要一个特点cars,表示用户具有的车辆,它是List类型。

运转代码,看看重组状况:

Jetpack Compose - 关于重组

能够看到,尽管cars一旦创立就无法修正,但终究UserInfo仍是产生了重组。

再看看编译器生成的日志,确认一下是不是unstable

Jetpack Compose - 关于重组

能够看到它的确被标记为unstable

在Kotlin中,尽管List接口没办法直接修正,可是它可能指向MutableList,例如:

val cars = mutableListOf("BMW")
val user = User(cars = cars)
// 在外部直接修正
cars.add("911")

这种状况,又会导致数据变了,UI没有及时变的问题,所以Compose默认这些调集接口的参数是unstable的。当然了,假如确认不会在外部去修正cars,能够给User加上@Immutable注解,像这样:

// 加上注解
@Immutable
data class User(
    val cars: List<String> = listOf("BMW")
)

这个注解比较好了解,顾名思义,表示不可变的,有爱好的读者能够看一下该注解的详细英文注释。

简单来说便是:假如某个Class被标记为@Immutable,意味着这个Class的目标一旦被创立,它一切public特点的内容都不会再改变了。

加上注解之后,再次运转代码,看看重组状况:

能够看到,现在UserInfo能够越过重组了。

再看看编译器生成的日志,确认一下是不是stable

Jetpack Compose - 关于重组

能够看到它的确被标记为stable了。

在实践开发中,假如给某个Class加上@Immutable注解,应该遵守不变的约好。

@Stable注解

最终,咱们单独看一下@Stable注解,这个注解也是和Compose约好状态为stable

先看一下注解源码:

@MustBeDocumented
@Target(  
    AnnotationTarget.CLASS,  
    AnnotationTarget.FUNCTION,  
    AnnotationTarget.PROPERTY_GETTER,  
    AnnotationTarget.PROPERTY  
)  
@Retention(AnnotationRetention.BINARY)  
@StableMarker  
annotation class Stable

相较于@Immutable@Stable注解的Target除了AnnotationTarget.CLASS之外,还支撑AnnotationTarget.FUNCTIONAnnotationTarget.PROPERTY_GETTER以及AnnotationTarget.PROPERTY

Target同为AnnotationTarget.CLASS时,它们有什么区别?

@Stable放松了限制,答应Class具有可变的public特点,前提是特点的改变能被Compose监测到。

先看看@Stable错误的用法

// 错误用法
@Stable
class User {  
    var name: String = "default"
}

这是一个错误的用法,由于它违反了约好:特点的改变能被Compose监测到

修正代码,遵守这个约好:

// 正确用法
@Stable
class User {
    // name的读写托付给了MutableState
    var name: String by mutableStateOf("default")
}

此刻,对name的读写,实践上是对MutableState的读写,由于这儿咱们运用了by托付。上文咱们现已说到过了,State的改变,Compose能够监测到。

最终,咱们再来看看@Stable注解用在AnnotationTarget.CLASS之外的场景。其实便是用在函数上面的场景,由于Kotlin特点的本质在Java看来便是GetterSetter

当函数的输入参数相一起,回来结果总是相同,而且输入的参数类型和回来的类型都是stable的。假如满意这个条件,就能够把该函数标示为@Stable

至于满意条件,标示@Stable后会有什么优化,作者暂时也不清楚,假如有知道的同学,费事谈论告知。

总结一下:

假如一个类被标记为@Immutable,那么它也能够被标记为@Stable。咱们应该优先运用@Immutable,只要@Immutable不满意的时分,例如有var的特点,且特点改变能够被Compose监测到,此刻咱们才能够用@Stable

完毕

以上便是全部内容,假如有错误的地方,还请读者谈论指出,一起学习,假如有任何问题,也能够加作者的微信讨论,感谢你的阅览。

作者微信:zj565061763