最近 Jetpack Compose 公布了 Beta 版本,抽时间理解了一下 Compose 带来的扭转和其中的一些原理。本文不会解说具体 API,只是比拟随便的分享本人的一些疑难以及在探寻答案过程中的一些播种。
为什么要有 Compose?
Android 曾经十年多了,传统的 Android UI ToolKit 有很多历史遗留问题,而有些官网也很难批改。比方 View.java 有三万多行代码,比方 Combo box 居然叫 Spinner,再比方 Button 继承自 Textview。同时官网的一些 widget 修复依赖系统升级,达到用户周期过长。
通过在 Jetpack 中增加 Compose,脱离了 Android 零碎,代码修复能够更快地达到用户。
而对国内开发者来说,更对立的代码,意味着没有厂商定制。这几天有位敌人和我埋怨『哪个大佬有工夫重写个 editText 吗,厂商 / 零碎的一堆问题』,我想他可能要梦想成真了。
同时,Compose 通过引入申明式编程,依赖 Kotlin 个性,能够让代码编写更快更简略。
设想写一个搜寻通讯录的界面,传统的 Android 开发写这个界面须要多少代码?activity 一个 xml,item 一个 xml,封装一个 recyclerview,再写一个 Adapter,写了这么多,可能还费劲不讨好,xml 转成 view 的过程中,IO 和反射影响了性能,界面再简单一些,走异步 layout 还是 x2c?而在 compose 中,可能只须要上面这段简短的代码,并且没有 xml 的性能问题。
如上图,在 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 是什么原理?
下面这个简略的例子,当点击 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.
Compose Compiler 的源码。ComposePlugin.kt 中注册了 ComposeIrGenerationextension。ComposeIrGenerationExtension 中又有 ComposableFunctionBodyTransformer 实现下面形容的办法中增加 start/end,ComposerLambdaMemoization 实现下面形容的扭转成 ComposerLambda。具体逻辑能够看源码,正文形容的比较清楚。
重组是怎么实现的?
看 Compose 的文档,始终有重组 (Recomposition) 这个词,就是状态变动的时候,自动更新 UI。那重组是怎么实现的呢?
每次调用 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()的时候,最终会调到 Composer 中的 recordModificationsOf 办法,而后从上段说的 Map 中获取 state 对应的 scope, 并把它增加到 invalidations 中,通过编舞者监听,下次 vsync 时,会调用 invalidations 中 lambda 的 invoke 办法,从而更新 UI。
请留神,『在调用 start 办法的时候,会生成一个 scope』,但其实只有第一次增加的时候生成就够了,前面更新 UI 的时候间接用旧的就能够了,太多相似的货色须要存储,Compose 中有一个十分重要的数据结构叫插槽表 SlotTable,刚说的这个 scope 复用以及例子中的 remember 都是利用了 SlotTable,具体能够看深刻详解 Jetpack Compose | 实现原理。
Text 对应的是 TextView 吗?
Text 对应的是 TextView 吗?不是的。
debug 看了一下,所有的 composable UI 最初被包在一个 AndroidComposeView 中,放在 ContentView 上面,所以最上层的货色是没有变动的。传统的 Android UI 中的 view 树,变成了 node 树,view 的那些性能被 node 代替了。
和旧有体系兼容,能够间接把 AndroidComposeView 增加在 xml 中,这样就能够混用了。
自定义 Layout 怎么写?
简略的看了一下,measure/layout 走的是 measurePolicy,在一个办法中去写 measure 和 layout。measure 中有个 Constraints 最大最小限度,相似 MeasureSpec 那一套,match\_parent 变成了 Modifier.fillMaxWidth(),这个 Modifier 会在 measure 之前批改 Constraints,measure 的时候会把批改后的 Constraints 传递进去。
draw 是通过 Modifier 实现的,还是走 canvas 那一套。
Touch 事件怎么解决?
自认为对 Android 的 touch 事件还算比拟理解,之前在看 Android 源码的时候也发现了一些有意思的中央,比方 down 事件在 native 底层解决,不是作为 message 在 java 层 looper 解决,所以 setMessageLogging 的形式检测不到 down 里的耗时。那编舞者不是散发 Input/animation/layout 的 callback 吗?那个次要是用来解决 move 事件。你认为 move 事件里只有一个坐标点吗,看看 MotionEvent.getHistorySize 办法吧,那这个 size 和屏幕采样率以及触控采样率又是什么关系呢?
言归正传,看到 Compose 的呈现,必定也好奇对 Touch 事件处理形式的扭转。
dispatchTouchEvent/onInterceptTouchEvent/onTouchEvent 这些办法不见了,拦挡事件须要实现 PointerInputFilter,次要逻辑写在 onPointerEvent 办法,这个办法甚至连 boolean 都没返回,那怎么判断是否生产了呢?传递进来包装好的 event 中有个是否生产的属性,每个 filter 本人判断是否有未生产的事件,去批改曾经生产。感觉这一块还有优化空间,如同没有生产之前的事件,后续事件还会回调到。
当初定义了 Initial、Main、Final 三个阶段,在你关怀的阶段中去解决,前两个阶段和以前差不多,Final 阶段相似用来解决之前的 cancel 事件。
结尾
Compose 还在继续优化中,比方 composable 函数最近要反对并发执行了。
两年磨一剑,谷歌推广 Compose 的信心是毋庸置疑的。Compose 为了不便开发者,也是思考到了很多事实的货色,比方像 kotlin 反对和 java 互调一样,反对 Compose 和传统 UI 互调。尽管投入微小,确实更快更简略,但在社区中的遍及还有待工夫验证,毕竟 Jetpack 中的库很多大家都还没有用过,而 Compose 的征程也注定要比 Kotlin 艰巨。
工夫无限,本文只能夸夸其谈、管中窥豹、抛砖引玉了,如有舛误,还望不吝赐教。