写点什么

Jetpack Compose 漫谈,搞懂这些直接来阿里入职

用户头像
Android架构
关注
发布于: 18 小时前


如上图,在 Compose 中,不断的方法调方法,就完成了 UI 的组装。


[](


)什么是声明式编程?




提到 Compose,就不得不了解什么是声明式编程。



我们来看一下维基百科的解释,声明式编程是一种编程范式,表达逻辑但不描述具体控制流程。就是告诉计算机我想要什么,而不是告诉它怎么做。那对应到 Android 就是我想要一个什么样的 UI,而不是这个 UI 应该如何改变,当然 UI 的自动改变需要框架的支持。


声明式编程在 React、Flutter 等框架中已经有广泛的应用,声明状态,状态变化,UI 自动重绘。



有意思的是,Compose 的发起人 Jim Sproch 之前是 React 的核心开发人员,他在 Slack 上聊到 VDOM 的一些问题,比如 vdom 分配内存空间在复杂项目中成为性能瓶颈,compose 采用调用 composable 方法的方式,减少内存分配。相比 vdom,compose 把 node 隐藏在背后以防滥用,同时可以更方便的使用 if/for 控制流程。


[](


)@Composable 是什么原理?




@Composable


fun CounterInner() {


val count = remember { (mutableStateOf(0)) }


Button(onClick = { count.value += 1 }) {


Text(text = "${count.value}")


}


}


上面这个简单的例子,当点击 button,button 中的文字自动加 1,remember 用来记录最新的 count 值。


@Composable 是个注解,而要实现自动更新 UI,肯定是修改了 Class 文件,让我们看看 class 文件变成了什么样?Kotlin 编译后的 class 在 build/tmp/kotlin-classes 目录中,但在 Android Studio 中是无法看到 class 反编译后的内容,可以用[Jadx](


)。然后 Text()这些 Composable 方法编译成 class 后也有改动,为了方便阅读,最好是编译好 APK 后,再用 Jadx 阅读反编译源码。



上面就是编译后的 CountInner 方法,可以看到,方法参数都被改变了,方法块中添加了很多 start/end,调用 Text()的 Lambda 变成了 ComposableLamda,改动还是比较多的。


这些改动是怎么实现的呢?如果我没记错的话,Kotlin 的协程也做了有些改变方法参数的操作,两个是不是差不多的实现?但协程是 kotlin 特性,应用层动态修改 class 文件,难道是在 Gradle Transform 里用 ASM 去操纵 class 的?


一番搜索,发现 Compose 应用了 Kotlin compiler 的新特性,通过 IR extension,可以在中间代码生成期间修改逻辑。IR 又是什么?intermediate representation 的缩写,翻译为中间语言。Kotlin 为了 Compose 开放了扩展能力,并且统一了 JVM/JS/Native 的 IR 流水线,为跨平台提供支持。可以理解为 Kotlin 对协程做的那些事情,通过使用 IR extension,你在应用层也可以去做了。


Talk is cheap, I will show you the code.



ComposeIrGenerationExtension 中又有 ComposableFunctionBodyTransformer 实现上面描述的方法中添加 start/end,ComposerLambdaMemoization 实现上面描述的改变成 ComposerLambda。具体逻辑可以看源码,注释描述的比较清楚。


[](


)重组是怎么实现的?




看 Compose 的文档,一直有重组(Recomposition)这个词,就是状态变化的时候,自动更新 UI。那重组是怎么实现的呢?


@Composable


fun CounterInner() {


val count = remember { (mutableStateOf(0)) }


Button(onClick = { count.value += 1 }) {


Text(text = "${count.value}")


}


}


每次调用 count.getValue()的时候,最终会回调到 Composer,Composer 中维持着一个 Map,这时就把 state 和当前的 scope 进行了关联,scope 可以理解为一段可以重组的范围。那当前的 scope 哪里来呢?还记得编译的 class 里多了很多 start 和 end 吗,在调用 start 方法的时候,会生成一个 scope,放在栈顶。所以调用 count.getValue()的时候,直接拿栈顶 scope 就可以了。当调用 end 的时候,会调用 updateScope 更新 scope 的 block 属性,而这个 block 是一个 lambda,执行这个 lambda 会调用对应的 composable 方法重绘,这样 state 和 block 就关联起来了,后面 state 变化的时候,拿到 block 执行就可以了。在这个例子中,count state 对应的 block 是一个调用 Button 方法的 lambda。


再来看下更新 state 的流程。每次调用 count.SetValue()的时候,最终会调到 Comp


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


oser 中的 recordModificationsOf 方法,然后从上段说的 Map 中获取 state 对应的 scope, 并把它添加到 invalidations 中,通过编舞者监听,下次 vsync 时,会调用 invalidations 中 lambda 的 invoke 方法,从而更新 UI。


请注意,『在调用 start 方法的时候,会生成一个 scope』,但其实只有第一次添加的时候生成就够了,后面更新 UI 的时候直接用旧的就可以了,太多类似的东西需要存储,Compose 中有一个非常重要的数据结构叫插槽表 SlotTable,刚说的这个 scope 复用以及例子中的 remember 都是利用了 SlotTable.

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Jetpack Compose漫谈,搞懂这些直接来阿里入职