Compose原理 - 整体架构与主流程
一、整体架构
在官方文档中(Jetpack Compose 架构层 | Android Developers),对Compose的分层有所阐述:
其中
Runtime:提供Compose的基础运行能力,包括State、Side-effects、CompositionLocal、Composition等等相关API。
UI:涉及到绘制的部分,主要是LayoutNode,Modifiler相关。
Foundation:提供Column、Row等“设计系统无关”的组件
Matieria:提供Material Design相关能力,比如主题、Color、Style等。
其中,Runtime的主要包含Composition的Composition, Recomposer, ComposeNode, 和RecomposeScope等主要组件。除此之外,还有State相关的Snapshot的部分。这些是Compose得以实现的核心。本文主要阐述的也就是这部分的架构。
Compose源码架构:
Jatpack Compose是以工具包的形式集成在Android的绘制体系中的。所以其本质上,是嵌入到了DecorView的Content中。换言之,Compose所组织的内容最终绘制在了ComposeView,再将ComposeView添加到DecorView中。如此Compose便集成到了Android的View体系。
在上图中,有几个重要的类:
Composition:可视为Composer的容器,主要负责与外部其他角色进行对接。内部包含了一个Composer。如上图,一般情况下,一个DecorView中包含一个Composition,即一个Activity对应一个Composition。如果使用了TabRow、BoxWithConstraints等组件,其内部会调用SubcomposeLayout,从而会创建多个子Composition实例。
Composer:代表一个Compose树,会内部对树的构建、重组等做一个整体的调度和管理
SlotTable:可以理解为实现Compose树的具体数据结构。其内部主要有两个数组成员:IntArray类型的groups和Array类型的slots。其中groups用来记录一个group的基本信息和其parent group,从而可以构建一个group树。而slots用来存储每个group内的具体数据。而对于这两个数组成员的具体操作,是通过Gap Buffer的方式进行处理的,这是一种在数据插入过程中移动Gap区域,而不是数组元素的方式来达到数据更新的目的。在Gap区域不移动的情况下,仅仅通过几个指针的操作来快速访问数据,由于Compose树的整体结构相对是稳定的,Gap的移动频率相对较少,因此大部分对数组的访问能达到O(1)的时间复杂度。从架构图看到,实际上每个Composition包含了两个SlotTable,一个用于写入,一个用于读取,在写入后通过同步机制将数据同步到用于写入的SlotTable中。读写的隔离有助于频繁写操作的过程中提高性能。
Group:SlotTable内的数据结构,通过内部的parent属性记录其父Group,从而整体上可以构建为一个树形结构。可以简单理解为,一个Composable(一个自定义或内置Composable函数)对应一个Group。不同类型的Composable对应不同的Group类型。因此一个Compose树,实际上就是Group树。
Recomposer:用于触发和管理Compose树的组建和重组。在Recomposer内部有一个while循环,由当前DecorView的viewTreeLifecycle的ON_CREATE时启动,随后如果有compositon被标记需要重组,则会进行重组流程,否则会挂起等待。
Snapshot:快照系统,用于对State值的版本进行管理。Snapshot的思想类似Git等版本管理系统,即,父分支快照基于自身拉出一个子分支快照,子快照只可以看到到自身快照内的State值的变更,对其他分支中相同State的变更不可见。子快照可以通过apply接口将自身变更合入到父分支快照。换言之,父快照的值对子快照是可见的,子快照对父快照是不可见的,一直到子快照apply()。每次重组都会创建一个快照,重组结束后会apply该快照到父快照,同时,快照也是ThreadLocal的,即是线程隔离的。这样,每次重组时所访问的State值的版本都是相互隔离的互不影响,在重组完成后再去merge。
RecomposeScope:Compose数在SlotTable中通过group来表达,但并非所有的group都是可重组的,group有很多类型。为了对可重组的group的范围进行标记圈定,在创建可重组group时会创建对应的RecomposeScope,并保存起来。当state变更导致重组时,会通过当前state对应的scope找到要重组的范围,进行重组。
Applier:SlotTable存储的只是存储了Compose树的结构信息以及树组合过程中涉及到的remember、state等数据信息,而具体的绘制是由LayoutNode树来负责的。Applier就是作为Compose树与LayoutNode之间的桥梁。当初次组件或者后续重组完成之后,会通过Applier通知到LayoutNode。随后LayoutNode根据提供的信息对发生了变化的Composable(保存在changes中)进行绘制。
ChangeList:Composition中changes变量所对应的具体类型。ChangeList存储Composable的具体变化,这些变化会通过Applier最终体现为LayoutNode树的变更。
LayoutNode:Composable在绘制层面的结构体。Compose树最终会转化为LayoutNode树,measure、draw都是在LayoutNode上进行的。
二、Compose运行流程
将Compose运行流程概述为:初始化、Composition(组合/重组并收集变更信息)、Applier(应用变更信息)、LayoutNode测量绘制
1. 初始化
Compose Runtime库中的ComponentActivity,提供了setContent扩展方法,用来支将ComposeView嵌入到Android固有的View体系中。ComposeView首先创建AndroidComposeView,并将AndroidComposeView通过addView加入到Android View树。Compose的设计是跨平台的,AndroidComposeView是针对Android平台的一些具体实现,Compose绘制的结果最终将体现在AndroidComposeView上,而AndroidComposeView的onMeasure和onLayout、dispatchDraw等也最终会传递给Compose体系中的LayoutNode,如此构成了Compose与Android固有View体系的结合。
ComposeView中,同时也会对Recomposer和Composition进行初始化。Recomposer用来重组管理,Composition代表Compose的组合树的容器。
2. Composition(“组合/重组“并变更信息)
2.1 首次组合
在上一步的setConent过程中,会调用Recomposer的composeInitial,开始对Compose树的初始组合,也就是构建Compose树的过程。
composing是Recomposer的重要方法,每次组合/重组的都会调用,可视为真正组合/重组的起点。本节中为了叙述方便,“组合”和“重组”统一表述为重组。在composing开始重组时,会首先通过Snapshot创建一个快照副本,后续本次重组所读写的State,都是在这个快照范围内进行的,不会影响其他快照。当然,其他快照也不会影响本次重组的快照,这样每次重组所使用的数据都是相互隔离的。
创建快照后,会开始调用Composition进行重组流程。每个Composition内有一个Composer类,执行具体重组动作。我们把所有@Composable修饰的自定义函数称作Composable,setContent中的content lambda也是一个Composable。在Composer中通过invokeComposable开始执行content lambda,由此开始一系列的Composable递归调用构建Compose树。我们以其中一个Composable为例,每个Composable执行过程中,会通过startXXXGroup在SlotTable中构建一个Group,这个Group将被保存在SlotTable的groups数组中。Group在不断创建过程中与Composable函数一样保持的对应的嵌入关系,也就是父子关系。后续会将Composable内相应的数据(Group本身数据,函数参数、remember、state值等)保存在这个Group对应的Slots数组内。
在创建完Group后,会通过addRecomposeScope创建一个RecomposeScope,这个RecomposeScope代表了本Group内state变更时所对应的变化范围。随后,这个scope本身也会作为对应Group的数据通过updateValue存到SlotTable的slots数据里。
以上完成后,会将scope通过insertSlots将其保存在changeListWriter中,通过scope可以找到对应的Group及其数据。所以本过程可以理解为将新建/变更的group暂存在changeListWriter中。
到此,通过Composable不断的递归调用,整个Compose树构建完毕,实际上就是StlotTable的Group树构建完毕。并且构建过程中的group信息(全量)本暂存changeListWriter,等待进行具体测量、布局和绘制。
2.2 重组
Compose的特点之一就是响应式编程,数据的变化驱动页面变化,这个过程称为重组。最长见的重组便是Composable内读取的state的值变更时导致的重组。过程如下:
在Recomposer每次重组时创建Snapshot时,会注册该Snapshot的read/wri
te监听。在重组过程中,如果遇到某个Composable读取了某个State,就会把该State存储到Composable对应的RecomposerScope内,并将该scope与state的对应关系存储在observations中。当对State进行write写入时, Recomposer会通过Snapshot的writeObserver监听到写入动作,并且通知Composition,Composition通过遍历observations取出state对应的scope,然后将其加入到invalidations缓存,随后也会将本composition作为invalid加入到Recomposer的compositionInvalidations缓存。
另一方面,Recomposer本身通过WindowRecomposer将自身与onCreate声明周期绑定,在onCreate时会运行runRecomposeAndApplyChanges函数,内部有一个while循环,运行频率会和vsync对齐。平时挂起,只要compositionInvalidations有了变更,就会开始运行。其运行过程为,将compositionInvalidations内的compostion取出, 调用其recompose函数。recompose内会将之前的invalidations内的scope取出,找到其对应的Group,调用Composer的doCompose对其进行变更。
state值的变更可能导致Composable位置移动,删除、插入等,这些都转化SlotTable对为Group的操作。在更新完SlotTable后,将Group的变更信息保存在changeListWriter。
从上面过程可以看出,state的变化并不会同步导致compose树立即更新,而是先存在
invalidations内,等待下一vsync信号时在处理。
3. Applier(应用变更信息)
现在,无论初始组合还是重组,最终的变更信息都会保存到changeListWriter。我们来看changeListWriter如何最终被转变为具体的页面变更。
在重组过程中每一个对Group的操作,最后会转变成一条指令通知给changeListWriter,比如新建一个Group,会调用changeListWriter的insertSlots函数,insertSlots会向ChangeList去push一条指令。这条指令保存在ChangeList的options中。Recomposer的runRecomposeAndApplyChanges在后续会调用composition.applyChanges,随后取出options的指令,进行执行。
在具体执行时,每种指令都具体转化为Options类的一个具体子类。例如创建一个Node Group的insertSlots,会由InsertNodeFixup来执行,随后通知作为root的LayoutNode,进行创建group对应的LayoutNode并加入其LayoutNode树的对应位置。
每种对Group的新建、移动、删除等操作,最后都变成了对响应layoutNode的新建、移动、删除操作。
4. LayoutNode测量、布局和绘制
LayoutNode结构变更完了,随后需要将其绘制出来。整个过程也主要分为Measure、Layout和Draw。MeasureAndLayoutDelegate是LayoutNode的测量代理类,执行其测量的具体方法。每次LayoutNode树变更时,会将变更的layoutNode存储在MeasureAndLayoutDelegate的relayoutNodes里。然后会通过传统的方式通知AndroidComposeView invalid,此时onMeasure会被回调。随后onMeasure会调用measureAndLayout取出MeasureAndLayoutDelegate的relayoutNodes缓存,然后进行从下到上的重新测量工作。Layout的过程也类似。
待测量布局完毕,AndroidComposeView的dispatchDraw被系统调用,此时,通过构建或更新LayoutNode对应的layer,并将其绘制在封装了Canvas的AndroidCanvas上。从而完成了绘制工作。
此处可见,绘制过程只对有变更的LayoutNode进行绘制,由此之前的重组的差量变更过程才有意义。
至此,Compose从初始化、重组到最终绘制的流程大致描述完毕。