大话Compose筑基(3)

365 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

往期文章
大话Compose筑基(1) - 掘金 (juejin.cn)
大话Compose筑基(2) - 掘金 (juejin.cn)

可组合项的生命周期

可组合项的生命周期通过以下事件定义:进入组合,执行 0 次或多次重组,然后退出组合。

image.png

正如管理状态文档中所述,一个组合将描述应用的界面,并通过运行可组合项来生成。组合是描述界面的可组合项的树结构。

当 Jetpack Compose 首次运行可组合项时,在初始组合期间,它将跟踪您为了描述组合中的界面而调用的可组合项。**然后,当应用的状态发生变化时,Jetpack Compose 会安排重组。**重组是指 Jetpack Compose 重新执行可能因状态更改而更改的可组合项,然后更新组合以反映所有更改。

组合只能通过初始组合生成且只能通过重组进行更新。重组是修改组合的唯一方式。

重组通常由对 State<T> 对象的更改触发。Compose 会跟踪这些操作,并运行组合中读取该特定 State<T> 的所有可组合项以及这些操作调用的无法跳过的所有可组合项。

所以通过了解可组合项的生命周期,我们就能更好的了解到可组合项在组合中的重组机制,从而更好的通过智能重组来规避非必要的性能耗损。

组合中可组合项的剖析

如果某一可组合项多次被调用,在组合中将放置多个实例。每个实例在组合中都有自己的生命周期。

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

这里的可组合函数TextColumn中调用了2次,分别构建了两个实例,实例的生命周期都是进入组合->初始化组合->退出组合。那么为什么同一个可组合函数调用了两次后,构建了两个实例而不是先构建一个实例再重组第一个实例呢?带着这个问题我们接着往下看。

  • 调用点标识

    组合中可组合项的实例由其调用点进行标识。Compose 编译器将每个调用点都视为不同的调用点。从多个调用点调用可组合项会在组合中创建多个可组合项实例。

    注意:某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。
    例子中的Text("Hello)并不一定会比Text("World")先执行,但是执行顺序并不影响我们调用时候的标记。

  • 额外信息标识

    如果调用点一样,不同的实例又是如何标识的呢?看下面的例子

    @Composable
    fun MoviesScreen(movies: List<Movie>) {
        Column {
            for (movie in movies) {
                //可组合项按照在列表循环中的角标的顺序在组合中构建实例
                MovieOverview(movie)
            }
        }
    }
    

    在上面的示例中,Compose 除了使用调用点之外,还使用执行顺序来区分组合中的实例。如果列表底部新增了一个 movie,Compose 可以重复使用组合中既有的实例,因为这些实例在列表中的位置没有发生变化,因此这些实例的 movie 输入是相同的。也就是说如果movies集合在尾部添加了一个Movie元素后发生了重组,之前的每一个MovieOverview都会使用同一实例,并且因为movie没有改变所以也会跳过重组,最后仅仅只会构建一个参数是集合添加进来的那个Movie对象的MovieOverview可组合项实例,并走它自己的生命周期。

    如果从同一个调用点多次调用某个可组合项,Compose 就无法唯一标识对该可组合项的每次调用,因此除了调用点之外,还会使用执行顺序来区分实例。这种行为有时是必需的,但在某些情况下会导致发生意外行为。上面的例子如果我们在集合的头部添加元素,而不是尾部添加。那么之前集合中的每一项的顺序都发生了变化,意味集合改变前的所有实例都会重新构建,每个可组合项都会走初始重组,可组合项内的附带效应也会重启(有些时候附带效应的操作成本是很高昂的)。

    @Composable
    fun MovieOverview(movie: Movie) {
        Column {
            //这里的附带效应是在后台下载一个网络上的图片供页面使用
            //重组的时候会取消当前协程,然后重启该协程执行里面的方法
            val image = loadNetworkImage(movie.url)
            MovieHeader(image)
    
            /* ... */
        }
    }
    

    理想情况下,我们认为 MovieOverview 实例的身份与传递到该实例的 movie 的身份相关联。如果我们对影片列表进行重新排序,理想情况下,我们会以类似方式在组合树中对实例进行重新排序,而不是将每个 MovieOverview 可组合项与不同影片实例进行重组。您可以使用 Compose 来告诉运行时,您要使用哪些值来标识树的给定部分:key 可组合项。

    通过调用带有一个或多个传入值的键可组合项来封装代码块,这些值将被组合以用于在组合中标识该实例。key 的值不必是全局唯一的,只需要在调用点处调用可组合项的作用域内确保其唯一性即可。在此示例中,每个 movie 都需要一个在 movies 的作用域内具有唯一性的 key;movie 也可以与应用中其他位置的其他可组合项共享该 key

    @Composable
    fun MoviesScreen(movies: List<Movie>) {
        Column {
            for (movie in movies) {
                key(movie.id) { // movie的唯一id
                    MovieOverview(movie)
                }
            }
        }
    }
    

    使用上述代码后,即使列表中的元素发生变化,Compose 也会识别未更改的 MovieOverview 实例,并且可重复使用它们;它们的附带效应将继续执行。

    一些可组合项提供对 key 可组合项的内置支持。例如,LazyColumn 接受在 items DSL 中指定自定义 key

    @Composable
    fun MoviesScreen(movies: List<Movie>) {
        LazyColumn {
            items(movies, key = { movie -> movie.id }) { movie ->
                MovieOverview(movie)
            }
        }
    }
    

小结

通过这篇我们了解到,在组合中调用可组合项会通过不同的标识机制来控制是否创建可组合项的实例,每个实例有着自己的生命周期,重组作为生命周期中的一环。所以如何调用可组合项会影响着可组合项的重组逻辑。那么除了调用方式对生命周期的影响从而影响着是否重组,还有其他因素决定着重组条件呢?比方说上面的例子,如果其中一个元素的id不变,标题变了。那么我们肯定希望此条可组合项重组。Compose又是如何实现这种类似recycleview里面的单条定点刷新的呢?带着这些问题,可以在下一篇讲的稳定类型@Stable 注解中找到答案。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情