大概在两年前在星河集团的左邻右家项目中,我就接触到了jetpack Compose,而且还项目中在逻辑简略的页面,运用了compose去完成。当时觉得很新颖,实践中也感觉到,这种呼应式的,与当时的Vue/微信小程序/Flutter中思维大同小异,或许是未来的一种原生写UI的趋势。在现在的每记和足迹项目中,新完成的页面,都会优先考虑用Compose去完成。但是,Compose的一些功用优化点及留意点,也是做为开发人员需求熟悉的,今日将做一个小的总结。

一、声明式 vs 指令式编程

1、界说

无论是官网文档仍是介绍Compose的长处时,都会说到Compose是声明式的。咱们来回顾下,在wiki上有着如下界说:

声明式编程(英语:Declarative programming)或译为声明式编程,是对与指令式编程不同的编程范型的一种合称。它们制作核算机程序的结构和元素,表达核算的逻辑而不必描绘它的控制流程[1]。

指令式编程(英语:Imperative programming);是一种描绘电脑所需作出的行为的编程范型。简直一切电脑的硬件都是指令式作业;简直一切电脑的硬件都是能履行机器语言,而机器代码是运用指令式的风格来写的。

通俗的来说便是:声明式编程是一种把程序写成描绘成果的形式,而不是怎么获得成果的形式。它首要重视成果,而不是完成细节。声明式编程的代码一般更简洁,更容易了解和维护。

指令式编程则是一种把程序写成指令的形式,告诉核算机怎么完成成果。它愈加重视细节,怎么完成使命。指令式编程的代码一般更长,更难了解和维护。

2、个人了解

Compose其实便是UI结构,它最首要的功用便是让开发人员愈加快速的完成 页面逻辑&交互效果 这是目的。

关于传统的XML来说,咱们通过恳求去服务器获取数据,恳求成功后,咱们需求findViewById找到页面元素View,再设置View的特点,更新页面展现状况。整个进程是按 http恳求 -> 呼应 -> 寻觅对应View -> 更新对应View墨守成规就地履行,这种思维便是指令式编程。

但是Compose描绘为 http恳求 -> 呼应 -> 更新mutableData -> 引证对应数据的View自动重组,整个进程不需求咱们开发去写更新UI的代码(发出指令),而是数据发生改动,UI界面自动更新,能够了解为声明式。

二、Compose优势

目前关于我的体会感受来说,Compose的优势体现在以下几个点:

  • 页面架构清晰。比照以前mvp,mvvm或结合viewbinding,少去了许多接口及编写填充数据相关的代码
  • 动画API简略好用。强壮的动画支持,使得写动画非常简略。
  • 开发效率高,写UI速度快,style、shape等样式运用简略。
  • 别的、还有一些官方优势介绍

三、Compose 的重组效果域

尽管Compose 编译器在背面做了大量作业来确保 recomposition 规模尽或许小,咱们仍是需求对哪些情况发生了重组以及重组的规模有必定的了解 。

假定有如下代码:

@Composable
fun Foo() {
    var text by remember { mutableStateOf("") }
    Log.d(TAG, "Foo")
    Button(onClick = {
        text = "$text $text"
    }.also { Log.d(TAG, "Button") }) {
        Log.d(TAG, "Button content lambda")
        Text(text).also { Log.d(TAG, "Text") }
    }
}

其打印成果为:

D/Compose: Button content lambda
D/Compose: Text

依照开发经历,榜首感觉会是,text变量只被Text控件用到了。

分析一下,Button控件的界说为:

Android jetpack Compose使用及性能优化小结

参数text作为表达式履行的调用途是 Button 的尾lambda,然后才作为参数传入Text()。 所以此刻最小重组规模是 Button 的 尾lambda 而非 Text()

别的还有两点需求重视:

  • Compose 关怀的是代码块中是否有对 state 的 read,而不是 write。
  • text 指向的 MutableState 实例是永远不会变的,变的仅仅内部的 value
重组中的 Inline 陷阱!

非inline函数才有资历成为重组的最小规模,了解这点特别重要!

咱们将代码稍作改动,为Text()包裹一个Box{...}

@Composable
fun Foo() {
    var text by remember { mutableStateOf("") }
    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Box {
            Log.d(TAG, "Box")
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}

日志如下:

D/Compose: Button content lambda
D/Compose: Box
D/Compose: Text

关键

  • ColumnRowBox甚至Layout这种容器类 Composable 都是inline函数,因而它们只能同享调用方的重组规模,也便是 Button 的 尾lambda

如果你期望通过缩小重组规模进步功用怎么办?

@Composable
fun Foo() {
    var text by remember { mutableStateOf("") }
    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text") }
        }
    }
}
@Composable
fun Wrapper(content: @Composable () -> Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}
  • 自界说非 inline 函数,使之满足 Compose 重组规模最小化条件。

四、Compose开发时,进步功用的重视点

当 Compose 更新重组时,它会经历三个阶段(跟传统View比较相似):

  • 组合:Compose 确定要显现的内容 – 运行可组合函数并构建界面树。
  • 布局:Compose 确定界面树中每个元素的尺寸和方位
  • 绘图:Compose 实际烘托各个界面元素。

根据这3个阶段, 尽或许从可组合函数中移除核算。每逢界面发生改动时,都或许需求从头运行可组合函数;或许关于动画的每一帧,都会从头履行您在可组合函数中放置的一切代码。

1、合理运用remember

它的效果是:

  • 保存重组时的状况,并能够有重组后取出之前的状况

引证官方的栗子:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}
  • LazyColumn在滑动时,会使自身状况发生改动导致ContactList重组,然后contacts.sortedWith(comparator)也会重复履行。而排序是一个占用CPU算力的函数,对功用发生了较大的影响。

正确做法:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, sortComparator) {
        contacts.sortedWith(sortComparator)
    }
    LazyColumn(modifier) {
        items(sortedContacts) {
          // ...
        }
    }
}
  • 运用remember会对排序的成果进行保存,使得下次重组时,只要contacts不发生改动 ,其值能够重复运用。
  • 也便是说,它只进行了一次排序操作,防止了每次重组时都进行了核算。

提示:

  • 更优的做法是将这类核算的操作移出Compose办法,放到ViewModel中,再运用collectAsStateLanchEffect等方式进行观测自动重组。
2、运用LazyColumn、LazyRow 等列表组件时,指定key

如下一段代码,是一个很常见的需求(from官网):

NoteRow记载每项记载的扼要信息,当咱们进入编辑页进行修正后,需求将最近修正的一条按修正时刻放到列表最前面。这时,假若不指定每项Item的Key,其间一项发生了方位改动,都会导致其他的NoteRow发生重组,但是咱们修正的仅仅其间一项,进行了不必要的烘托。

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

正确的做法:

  • 为每项Item供给 项键,就可防止其他未修正的NoteRow只需移动方位,防止发生重组
@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
             key = { note ->
                // 为每项Item供给稳定的、不会发生改动的唯一值(一般为项ID)
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}
3、运用derivedStateOf约束重组

假定咱们需求根据列表的榜首项是否可见来决议划到顶部的按钮是否可见,代码如下:

val listState = rememberLazyListState()
LazyColumn(state = listState) {
    // ...
}
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}
  • 因为列表的滑动会使listState状况改动,而运用showButtonAnimatedVisibility会不断重组,导致功用下降。

解决方案是运用派生状况。如下 :

val listState = rememberLazyListState()
LazyColumn(state = listState) {
  // ...
  }
val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}
AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}
  • 派生状况,能够这样了解,只有在derivedStateOf里的状况发生改动时,只重视和派发对UI界面发生了影响的状况。这样AnimatedVisibility只会在改动时发生重组。对应的运用场景是,状况发生了改动,但是咱们只重视对界面发生了影响的状况进行分发,这种情况下,就能够考虑运用。
4、尽或许推延State的读行为

之前咱们提到,关于一个Compose页面来说,它会经历以下进程:

  • 榜首步,Composition,这其实就代表了咱们的Composable函数履行的进程。
  • 第二步,Layout,这跟咱们View系统的Layout相似,但整体的分发流程是存在一些差异的。
  • 第三步,Draw,也便是绘制,Compose的UI元素最终会绘制在Android的Canvas上。由此可见,Jetpack Compose尽管是全新的UI结构,但它的底层并没有脱离Android的范畴。
  • 最终,Recomposition,也便是重组,而且重复1、2、3进程。

尽或许推延状况读取的原因,其实仍是期望咱们能够在某些场景下直接越过Recomposition的阶段、甚至Layout的阶段,只影响到Draw。

分析如下代码:

@Composable
fun SnackDetail() {
    // Recomposition Scope
    // ...
    Box(Modifier.fillMaxSize()) {  Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value) // 1,状况读取
        // ...
    } 
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状况运用
    ) {
        // ...
    }
}

上面的代码有两个注释,注释1,代表了状况的读取;注释2,代表了状况的运用。这种“状况读取与运用方位不一致”的现象,其实就为Compose供给了功用优化的空间。

那么,具体咱们该怎么优化呢?简略来说,就是让:“状况读取与运用方位一致”

改为如下 :

// 代码段12
@Composable
fun SnackDetail() {
    // Recomposition Scope 
    // ...
    Box(Modifier.fillMaxSize()) {
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value } // 1,Laziness
        // ...
    } 
    // Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状况读取+运用
    ) {
    // ...
    }
}

了解: 因为咱们将scroll.value变成了Lambda,所以,它并不会在composition期间发生状况读取行为,这样,当scroll.value发生改动的时分,就不会触发「重组」,这便是 推延 的意义。

Android jetpack Compose使用及性能优化小结

五、小结

其实以上案例优化的点在本质上,都是在饯别:状况读取与运用方位一致的原则。但是需求咱们对Compose的底层原理,快照系统,还有ScopeUpdateScope有必定的了解。这样才会让咱们有着深刻的了解,代码为什么要这么写。后续,我也会更新Compose原理类的文章,敬请重视。