一、智能的重组
传统视图中经过修正View
的私有特点来改动UI
, Compose
则经过重组改写UI
。Compose
的重组非常“智能”,当重组产生时,只有状况产生更新的Composable
才会参加重组,没有改动的Composable
会越过本次重组。
二、防止重组的“圈套”
因为Composable
在编译期代码会产生改动,代码的实践运转状况或许并不如你预期的那样。所以需求了解Composable
在重组履行时的一些特性,防止落入重组的“圈套”。
1、Composable会以恣意次序履行
首要需求留意的是当代码中呈现多个Composable
时,它们并纷歧定按照代码中呈现的次序履行。比如,在一个Navigation
中处于Stack
最上方的UI
会优先被绘制,在一个Box
布局中处于远景的UI
具有较高的优先级,因而Composable
会依据优先级来履行,这与代码中呈现的方位或许并不共同。
Composable
都应该“自给自足”,不要试图经过外部变量与其他Composable
产生相关。在Composable
中改动外部环境变量归于一种“副效果”行为,Composable
应该尽量防止副效果。
2、Composable会并发履行
重组中的Composable
并纷歧定履行在UI
线程,它们或许在后台线程池中并行履行,这有利于发挥多核处理器的功能优势。可是因为多个Composable
在同一时间或许履行在不同线程,此刻有必要考虑线程安全问题。看看下面EventsFeed
的比如:
@Composable
fun EventsFeed(localEvents: List<Event>, nationalEvents: List<Event>) {
var totalEvents = 0
Row {
Column { //column-content-1
localEvents.forEach { event ->
Text("Item: $ {event.name}")
totalEvents++
}
}
Spacer(Modifier.height(10.dp))
Column { //column-content-2
nationalEvents.forEach { event ->
Text("Item: $ {event.name}")
totalEvents++
}
}
Text(
if (totalEvents == 0) "No events." else "Total events StotalEvents"
)
}
}
本例想运用totalEvents
记载events
的合计数量并在Text
显示,column-content-1
和column-content-2
有或许在不同线程并行履行,所以totalEvents
的累加对错线程安全的,成果或许不精确。即使totalEvents
的成果精确,因为Text
或许运转在单独线程,所以也纷歧定能正确显示成果,这相同仍是Composable
的副效果带来的问题,我们需求极力防止。
3、Composable会重复履行
除了重组会形成Composable
的再次履行外,在动画等场景中每一帧的改动都或许引起Composable
的履行,因而Composable
有或许会短时间内重复履行,咱们无法精确判别它的履行次数。我们在写代码时有必要考虑到这一点:即使多次履行也不应该呈现功能问题,更不应该对外部产生额外影响。来看下面的比如:
@Composable
fun EventsFeed(networkService: EventsNetworkService) {
//异步恳求数据
val events = networkService.loadAllEvents()
LazyColumn {
items(events) { event ->
Text(text = event.name)
}
}
}
在EventsFeed
中,loadAllEvents
是一个IO
操作,履行本钱高,假如在Composable
中同步调用,会在重组时形成卡顿。也许有人会提出将数据恳求逻辑放到异步线程履行,以进步功能。这儿测验将数据恳求的逻辑移动到ViewModel
中异步履行,防止堵塞中线程:
@Composable
fun EventsFeed(viewModel: EventsViewModel) {
//异步履行成果回调到主线程并将Flow转换为State
val events = viewModel.loadAllEvents().collectAsState(emptyList())
LazyColumn {
items(events) { event ->
Text(text = event.name)
}
}
}
尽管没有了同步IO
的烦恼,可是events
的更新会触发EventsFeed
重组,从而形成loadAllEvents
的再次履行。loadAllEvents
作为一个副效果不应该跟从重组重复调用,Compose
中供给了专门处理副效果的办法,这个会在后边介绍。
4、Composable的履行是“达观”的
所谓“达观”是指Composable
终究总会依据最新的状况正确地完结重组。在某些场景下,状况或许会接连改动,这或许会导致中间态的重组在履行中被打断,新的重组会刺进进来。关于被打断的重组,Compose不会将履行一半的重组成果反应到视图树上,因为它知道最终一次状况总之是正确的,因而中间状况会被丢掉。
@Composable
fun MyList{
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// 展现列表项 ...
}
//上报列表展现元素用作数据剖析
MyAnalyticsService.sendVisibleItem(listState.layoutInfo.visibleItemsInfo)
}
在上面的代码中,MyList
用来显示一个列表数据。这儿试图在重组过程中将列表中展现的项目信息上报服务器,用作产品剖析,但这样写是很风险的,因为任何时候我们都无法确认重组能够被正常履行而不被打断。假如此次重组被打断了,那么会呈现数据上报内容与实践视图不共同的问题,影响产品剖析数据。
像数据上报这类会对外界产生影响的逻辑称为副效果。Composable
中不应该直接呈现任何对状况有依靠的副效果代码,当有相似需求时,应该运用Compose
供给的专门处理副效果的API
进行包裹,例如SideEffect{...}
等,副效果能够在里面安全地履行并获取正确的状况。
针对本末节的介绍得出一个定论:Compose
结构要求Composable
作为一个无副效果的纯函数运转,只需在开发中遵从这一准则,上述这一系列特性就不会成为程序履行的“圈套”,反而有助于进步程序的履行功能。
三、怎么确认重组规模
咱们知道重组是智能的,会尽或许越过不必要的重组,只是针对需求改动的UI进行重组。那么Compose
怎么认定UI需求改动呢?或许说Compose
怎么确认重组的最小规模呢?看下面的代码:
@Composable
fun Greeting() {
Log.d(TAG, "Scope-1 run") //日志一
var counter by remember { mutableStateOf(0) }
Column { //Scope-2
Log.d(TAG, "Scope-2 run") //日志二
Button(
onClick = run {
Log.d(TAG, "Button-onClick") //日志三
return@run { counter++ }
}
) { //Scope-3
Log.d(TAG, "Scope-3 run") //日志四
Text("+")
}
Text("$counter")
}
}
运转后会得到下面的日志:
//运转后,点击按钮前
Scope-1 run
Scope-2 run
Button-onClick
Scope-3 run
//点击按钮后
Scope-1 run
Scope-2 run
Button-onClick
Scope-3 run
第一条日志竟然不是Button-onClick
,这是为什么呢?
经过Compose
编译器处理后的Composable代码在对State进行读取的一起能够主动树立相关,在运转过程中当State
改动时,Compose
会找到相关的代码块标记为Invalid
。在下一烘托帧到来之前,Compose
会触发重组并履行invalid
代码块,Invalid
代码块即下一次重组的规模。能够被标记为Invalid
的代码有必要对错inline
且无回来值的Composable
函数或lambda
,假如是inline
的,则在编译时会被打开,无法确认重组规模。
只有受到State改动影响的代码块,才会参加到重组,不依靠State的代码则不参加重组,这就是重组规模的最小化准则。
那么参加重组的代码块为什么有必要是inline
的无回来值函数呢?因为inline
函数在编译期会在调用处打开,因而无法在下次重组时找到合适的调用进口,只能同享调用方的重组规模。而关于有回来值的函数,因为回来值的改动会影响调用方,所以有必要连同调用方一同参加重组,因而它不能单独作为Invalid
代码块。
剖析一下上面的日志:
按照重组最小化准则,拜访counter
的最小规模应该是Scope2
,为什么Scope1-run
也输出了呢?还记得最小化规模的界说有必要对错inline
的Composable
函数或lambda
吗?Column
实践上是个inline
声明的高阶函数,内部content
也会被打开在调用处,Scope-2
与Scope-1
同享重组规模,Scope-1 run
日志被输出。看下Column
源码:
@Composable
inline fun Column(...){...} //inline
假如咱们将Column
修正为非inline
的Card
成果就会纷歧样,先看一下Card
的源码:
@Composable
@NonRestartableComposable
fun Card(...){...} //非inline
修正为Card
后的代码和日志:
@Composable
fun Greeting() {
Log.d(TAG, "Scope-1 run")
var counter by remember { mutableStateOf(0) }
Card { //Scope-2 <---------将Column修正为非inline的Card
Log.d(TAG, "Scope-2 run")
Button(
onClick = run {
Log.d(TAG, "Button-onClick")
return@run { counter++ }
}
) { //Scope-3
Log.d(TAG, "Scope-3 run")
Text("+")
}
Text("$counter")
}
}
//运转后,点击按钮前
Scope-1 run
Scope-2 run
Button-onClick
Scope-3 run
//点击按钮后
Scope-2 run
Button-onClick
Scope-3 run
重组规模已经被限定在非inline
且无回来值的函数Card
内部,不再输出Scope-1 run
。
四、优化重组的功能
Composable
经过履行之后会生成一颗视图树,每个Composable
对应了树上的一个节点。因而Composable
智能重组的本质其实是从树上寻觅对应方位的节点并与之进行比较,假如节点未产生改动,则不必更新。
弥补提示:
视图树构建的实践过程比较复杂,Composable
履行过程中,先将生成的Composition
状况存入SlotTable
,而后结构依据SlotTable
生成LayoutNode
树,并完结终究界面烘托。谨慎来说,Composable
的比较逻辑产生在SlotTable
中,并非是Composable
在履行中直接与视图树节点作比较。
1、Composable的方位索引
在重组过程中,Composition
上的节点能够完结增、删、移动、更新等多种改动。Compose
编译器会依据代码调用方位,为Composable
生成索引key
,并存入Composiitoin
。Compoable
在履行中经过与key
的对比,能够知道当时应该履行何种操作。
(1)怎么了解这个key
?
Compose
编译器会依据 Composable
函数在代码中被调用的方位来生成一个仅有的索引 key
。假如同一个函数被调用二次,会生成二个不同的索引key
。
(2)索引key
是怎么生成的?
Compose
编译器会运用以下算法来生成索引 key
:
- 首要,
Compose
编译器会为每个Composable
函数生成一个仅有的ID
。这个ID
由Composable
函数的名称和参数列表组成。 - 然后,
Compose
编译器会将这个ID
转换为一个十六进制字符串。 - 最终,
Compose
编译器会将这个十六进制字符串加上一个随机数作为索引key
。
(3)索引 key
有什么效果?
索引 key
用于在 Compose
的布局树中仅有标识一个 Composable
函数。Compose
会运用索引 key
来确认一个 Composable
函数是否已经被烘托过,以及怎么更新已经烘托过的 Composable
函数。
(4)哪些状况下会增加索引key
?
在 Jetpack Compose
中,Composable
函数的烘托成果会被增加到一个布局树中。假如 Composable
函数的烘托成果是可变的,Compose
会运用索引 key
来辨认该节点是否已经被烘托过,以及怎么更新已经烘托过的节点。
例如在 Composable
函数中运用 if/else
等条件句子时,Compose
编译器会在条件句子前刺进 startXXXGroup()
代码,并为每个条件分支增加一个仅有的索引 key
。这样,Compose
就能够在布局树中辨认条件句子导致的节点增减。示例代码:
@Composable
fun MyComposable() {
val isEnabled = true
if (isEnabled) {
// 烘托内容 1
// 增加索引 key 1
startScope {
// ...
}
} else {
// 烘托内容 2
// 增加索引 key 2
startScope {
// ...
}
}
}
除了函数节点,条件句子节点外,Compose
编译器还会在以下状况下增加索引 key
:
- 运用
remember()
函数来缓存数据时。 - 运用
mutableStateOf()
函数来创立可变状况时。 - 运用
MutableList()
或MutableMap()
等可变调集或映射时。
总体来说,Compose
会在任何或许导致布局树中节点增减的地方增加索引 key
。
(5)手动增加索引key
进步功能
先看下面的代码:
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
//MovieOverview无法在编译期进行索引
//只能依据运转时的index进行索引
MovieOverview(movie)
}
}
}
在上面的代码中,依据Movie
列表数据展现MovieOverview
。此刻无法依据代码中的方位进行索引,只能在运转时依据index进行索引。这样的索引会依据item
的数量改动而产生改动,无法精确匹配目标进行比较。如下图所示,当时MoviesScreen已经有两条数据,当在头部再刺进一条数据时,之前的索引产生过错,无法在比较时起到锚定原目标的效果。
当重组产生时,新刺进的数据会与曾经的0
号数据比较,曾经的0
号数据会与曾经的1
号数据比较,曾经的1
号数据作为新数据刺进,成果所有item
都会产生重组,但咱们期望的行为是,仅新刺进的数据需求组合,其他数据因为没有改动不应该产生重组。——图中灰色的部分表示参加重组的item
。
此刻能够运用key
办法为Composable
在运转时手动树立仅有索引,代码如下:
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id){
//运用Movie的仅有id作为Composable的索引
MovieOverview(movie)
}
}
}
}
运用Movie.id
传入Composable
作为仅有索引,当刺进新数据后,之前目标的索引没有被打乱,依然能够发挥比较时的锚定效果,如下图所示,之前的数据没有产生改动,对应的Item无须参加重组。
二、活用@Stable或@Immutable
Composable
依据参数的比较成果来决议是否重组。更精确地说,只有当参加比较的参数目标是安稳的且equals回来true,才认为是持平的。
那么什么样的类型是安稳的呢?比如Kotlin
中常见的根本类型(Boolean
、Int
、Long
、Float
、Char
)、String
类型,以及函数类型(Lambda
)都能够称得上是安稳的,因为它们都是不可变类型,它们参加比较的成果是永久可信的。反之,假如参数是可变类型,它们的equlas
成果将不再可信。
下面的比如明晰地展现了这一点:
//MutableData类
class MutableData(var data: String)
//MutableDemo函数
@Composable
fun MutableDemo() {
val mutable = remember { MutableData("Hello") }
var state by remember { mutableStateOf(false) }
if (state) {
mutable.data = "World"
}
//WrapperText显示会随着state的改动而改动
Button(onClick = { state = !state }) {
WrapperText(mutable)
}
}
//WrapperText函数
@Composable
fun WrapperText(mutableData: MutableData) {
Text(text = mutableData.data)
}
上述代码中,MutableData
是一个“不安稳”的目标,因为它有一个var
类型的成员data
,当单击Button
改动状况时,mutable
修正了data
。关于WrapperText
来说,参数mutable
在状况改动前后都指向同一个目标,因而只是靠equals
判别会认为是参数没有改动。但实践测验后会发现WrapperText
的重组依然产生了,因为关于Compiler
来说,MutableData
参数类型是不安稳的,equals
成果并不可信。
关于一个非根本类型T
,不管它是数据类仍是普通类,若它的所有public
特点都是final
的不可变类型,则T
也会被Compiler
辨认为安稳类型。此外,像MutableState
这样的可变类型也被视为安稳类型,因为它的value
的改动能够被追寻并触发重组,相当于在新的重组产生之前坚持不变。
关于一些默许不被认为是安稳的类型,比如interface
或许List
等调集类,假如能够保证其在运转时的安稳,可认为其增加@Stable
注解,编译器会将这些类型视为安稳类型,从而发挥智能重组的效果,提升重组功能。需求留意的是,被增加@Stable
的普通父类、密封类、接口等,其派生子类也会被视为是安稳的。
如下面的代码所示,当运用interface
界说UiState
时,可认为其增加@Stable
,当在Composable
中传入UiState
时,Composable
的重组会更加智能。
//增加注解,告知编译器其类型是安稳的,能够越过不必要的重组
@Stable
interface UiState<T> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
除了@Stable
外,Compose
还供给了另一个相似的注解@Immutable
。两者都承继自@StableMarker
,在功能上相似,都是用来告知编译器所注解的类型能够越过不必要的重组。
不同点在于,@Immutable
润饰的类型应该是完全的不可变类型,@Stable
润饰的类型中能够存在可变类型的特点,但只需特点的改动是能够观察的(能够触发重组,例如MutableStable<T>
等),依然被视作安稳的。另外在运用注解规模上,@Stable
能够用在函数、特点等更多场景,可是总体上@Stable
的才能完全覆盖了@Immutable
。因为功能的堆叠,未来@Immutable
有或许会被移除,主张我们优先选择运用@Stable
。
参阅了以下内容:
本文大部分内容参阅了实体书 Jetpack Compose从入门到实战,我们多多支撑实体书
其他参阅内容:
初学者如有过错欢迎批评指正!