查看原文
其他

你需要了解的Compose背后的技术原理

杜梦南 Jetpack Compose 博物馆 2022-07-13

作者:杜梦南

个人介绍:android-dev-challenge 2021挑战赛全球前500名,Compose开发者,Jetpack组件包推广者,IReader高级开发

个人主页:https://juejin.cn/user/413072100166797

揭秘 @Composable

添加了@Composable注解的函数会使函数类型改变,其内部依赖于贯穿整个函数作用域的Composer。@Composable 的特点如下:

  • @Composable 本质并不是一个注解处理器,Compose 在 Kotlin 编译器的类型检测与代码生成阶段依赖 Kotlin 编译器插件工作,所以无需注解处理器即可使用 Compose。

  • @Composable 会导致它类型的改变,未被注解的相同函数类型与注解后的类型互不兼容

    @Composable 会辅助kotlin编译器知晓,此函数用于将数据转换为UI界面,即用于描述屏幕的显示状态

  • @Composable 并非语言特性,无法采用语言关键字的形式进行实现

接下来我们根据一个最简单的函数分析其内部实现。

kotlin代码如下:

@Composable
fun HelloWord(text: String) {
    Text(text = text)
}

反编译代码如下:

public static final void HelloWord(String text, Composer $composer, int $changed) {
        int i;
        Composer $composer2;
        Intrinsics.checkNotNullParameter(text, "text");
        Composer $composer3 = $composer.startRestartGroup(1404424604,"C(HelloWord)7@159L17:Hello.kt#nlh07n");
        if (($changed & 14) == 0) {
            i = ($composer3.changed(text) ? 4 : 2) | $changed;
        } else {
            i = $changed;
        }
        if (((i & 11) ^ 2) != 0 || !$composer3.getSkipping()) {
            $composer2 = $composer3;
            TextKt.m855Text6FffQQw(text, null, Color.m1091constructorimpl(ULong.m2915constructorimpl(0)), TextUnit.m2481constructorimpl(0), nullnullnull, TextUnit.m2481constructorimpl(0), nullnull, TextUnit.m2481constructorimpl(0), nullfalse0nullnull, $composer2, i & 14065534);
        } else {
            $composer3.skipToGroupEnd();
            $composer2 = $composer3;
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup != null) {
            endRestartGroup.updateScope(new HelloKt$HelloWord$1(text, $changed));
        }
    }

函数内部的概念点比较多,我们逐一分析。

Composer

Compose其实是贯穿整个Composable函数作用域的上下文,提供了一些创建内部Group等方法。内部的数据结构是Slot Table,其特点如下:

  • Slot Table 是一个在连续空间中存储数据的类型,底层是数组实现。但是区别在于它的剩余空间,称为 Gap

    • Gap具有移动到任何区域的能力,所以在数据插入与删除时更高效。
  • Slot Table 其本质是一个线性的数据结构,所以支持将View树存储在 Slot Table 中。

    • 根据 Slot Table 可移动插入点的特性,让View树在变动之后无需重新创建整个View树的数据结构。
  • 虽然Slot Table 相比普通数组拥有了任意位置插入数据的能力,但是 Gap 移动仍是低效操作

    • Google 判断在大概率情况下的界面更新是数据变更View树结构并不会经常发生变动
    • 并且基本上只有数组这种内存连续的数据结构在访问效率上才能达到 Compose Runtime 的要求

正是所有的Composeable函数内部都依赖于Composer,所以非Composeable函数是无法调用Composeable函数的

此时我们逆推,尝试推理几种结果:

  • 如果使用链表的话,插入的时间复杂度=o(1),但是查找的时间复杂度=o(n),

  • 如果使用数组的话,查找的时间复杂度=o(1),插入的时间复杂度=o(n)度。

除了gap buffer还有其他解决方案:

  1. 块状链表(可以在o(n^1/2)复杂度完成插入和查找)
  2. Rope树(一种平衡查找树)
  3. piece table(改进版本的gap buffer,微软doc就是使用的这个算法,同时可以快速实现撤销和重组)

逻辑代码如下:

int GAP_SIZE = 5;//默认gap 大小

//基本结构
struct GapBuffer{
 char array[size];
 int gapStart;
 int gapSize;
 int len;
} gapBuffer;

//插入逻辑实现
void insert(char c){
 if(gapBuffer.gapSize<0)
  expanison();
 gapBuffer.array[++gapBuffer.gapStart]=c;
 --gapBuffer.gapSize;
 ++len;
}

//扩容逻辑实现
void expanison(){
  //扩容两倍增长
 GAP_SIZE = GAP_SZIE*2;
 gapBuffer.gapSize = GAP_SIZE;
  //将数据后半段向后复制,给予GAP_SiZE空间
 arraycopy(gapBuffer.array,gapBuffer.gapStart,gapBuffer.gapStart+gapBuffer.gapSize,len-gapBuffer.start);
}

//移动gap逻辑实现
//不会扩容buffer
void moveGap(int pos){
  //相同位置不做移动
 if(gapBuffer.gapStart == pos)return;
 
 //如果pos小于当前gap
 if(pos<gapBuffer.gapStart)
    //copy数组
  arraycopy(gapBuffer.array,pos,pos+gapBuffer.gapSize,gapBuffer.gapStart-pos);
 else
    //copy数组
  arraycopy(gapBuffer.array,gapBuffer.gapStart+gapBuffer.gapSize,gapBuffer.gapStart,gapBuffer.gapSize);
 
}
//数组拷贝逻辑实现
void arraycopy(char array[],int srcSatrt,int dstStart,int len){
 for(int i = 0;i<len;++i)
  array[dstStart+i]=array[srcStart+i];
}

UI的承载结构本质是树形结构,测量布局渲染都是对UI树进行深度遍历。

Group创建以及重组逻辑

Group创建

  • Group是根据startRestartGroupendRestartGroup方法进行创建的,最终创建于Slot Table之中
  • 创建的Group是用来管理UI的动态处理(即数据结构视角移动和插入
    • 创建完毕的Group会使编译器了解哪些开发者的代码会改变哪些UI的结构

重组

Compose Runtime 会根据数据影响范围确定需要重新执行的可组合函数,这一步骤被称为重组

无论Composable结构多么庞大,Composable函数都可以在任何时候被重新调用重组

根据其特性,当其中的某一部分发生变化时,Composable函数并不会重新计算整个层级结构

Composer 可以根据是否正在修改UI而确定具体的调用

数据更新导致部分UI刷新

场景Composer 代码处理说明
数据更新导致部分UI刷新1. Composer.skipToGroupEnd() 直接跳到当前Group末端非刷新部分不在重新创建,而是跳过重新绘制,直接访问

2. Composer.endRestartGroup()返回ScopeUpdateScope类型对象最终传入了调用当前可组合函数的 LambdaCompose Runtime 可根据当前环境确定可组合函数的调用范围

Composer 对 Slot Table 的操作是读写分离的,只有写操作完成后才将所有写入内容更新到 Slot Table 中

上文只解释了重组的上层代码调用,其实内部是依赖于State数据状态管理Positional Memoization

State

Compose 是一个声明式框架,State 采用观察者模式来实现界面随数据自动更新

包含状态管理的简单函数实现:

@Composable
fun Content() {
    var state by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { state++ }) { Text(text = "click to change state")}
        Text("state value: $state")
    }
}

remember()

remeber()是一个Composable函数,内部实现类似于委托,实现了Composable函数调用链中的对象记忆

  • Composable函数在调用链位置不变的情况下,调用 remember() 即可获取上次调用时记忆的内容。
  • 同一个Composable函数在不同位置被调用,其remember()函数获取的内容也不同。

同一个Composable函数被多次调用,将会产生多个实例。每次调用都有其自己的生命周期

mutableStateOf

  • mutableStateOf其实真正的内部实现是SnapshotMutableStateImpl
fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()

): MutableState<T> = SnapshotMutableStateImpl(value, policy)
  • 除了传入的value,还有Policy(处理策略)
  • 处理策略用于控制mutableStateOf()传入的数据,以何种方式进行汇报(观察到的时机),策略类型如下:
    • structuralEqualityPolicy(默认策略)
    • referentialEqualityPolicy(平等策略)
    • 还可以自定义接口实现策略
private class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if (!policy.equivalent(it.value, value)) {
                next.writable(this) { this.value = value }
            }
        }
  //当前state状态信息
    private var next: StateStateRecord<T> = StateStateRecord(value)

    override val firstStateRecord: StateRecord
        get() = next

    override fun prependStateRecord(value: StateRecord) {
        @Suppress("UNCHECKED_CAST")
        next = value as StateStateRecord<T>
    }

    @Suppress("UNCHECKED_CAST")
    override fun mergeRecords(
        previous: StateRecord,
        current: StateRecord,
        applied: StateRecord
    )
: StateRecord? {
        val previousRecord = previous as StateStateRecord<T>
        val currentRecord = current as StateStateRecord<T>
        val appliedRecord = applied as StateStateRecord<T>
        return if (policy.equivalent(currentRecord.value, appliedRecord.value))
            current
        else {
            val merged = policy.merge(
                previousRecord.value,
                currentRecord.value,
                appliedRecord.value
            )
            if (merged != null) {
                appliedRecord.create().also {
                    (it as StateStateRecord<T>).value = merged
                }
            } else {
                null
            }
        }
    }

    private class StateStateRecord<T>(myValue: T) : StateRecord() {
        override fun assign(value: StateRecord) {
            @Suppress("UNCHECKED_CAST")
            this.value = (value as StateStateRecord<T>).value
        }

        override fun create(): StateRecord = StateStateRecord(value)

        var value: T = myValue
    }
}
  • SnapshotMutableStateImpl内部会对写入至Composer进行一部分处理(数据对比,数据合并,读取,写入等操作)

    处理的逻辑是根据上文介绍的策略所进行

数据通知

在SnapshotMutableStateImpl的单独set()方法之中,对其完成了观察者的通知。具体流程如下:

获取Snapshot
inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.writableRecord(state, snapshot).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}

调用block其实是为了在第一次状态记录中,直接控制可写

  • Snapshot.current获取当前的Snapshot,具体场景分析如下:
    • 如果通过异步操作更新,因为 Snapshot 是一个 ThreadLocal ,所以会返回当前执行线程的 Snapshot
    • 如果当前执行线程的 Snapshot 为空时默认返回 GlobalSnapshot
    • 如果在Composable中直接对 mutableState 进行更新操作,当前 Composable 执行线程的 Snapshot 就是 MutableSnapshot
控制写入|存入至modified
  • 获取到snapshot之后,要对其进行写入
internal fun <T : StateRecord> T.writableRecord(state: StateObject, snapshot: Snapshot): T {
    ........
    snapshot.recordModified(state)
    return newData
}
  • 最终通过recordModified实现写入
override fun recordModified(state: StateObject) {
    (modified ?: HashSet<StateObject>().also { modified = it }).add(state)
}
  • 将当前修改的 state 添加到当前 Snapshot 的 modified 中
观察者通知
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}
  • 最终调用notifyWrite方法完成观察者的通知
Kotlin函数区别
函数结构体函数内对象返回值是否是扩展函数场景
letfun <T, R> T.let(block: (T) -> R): R = block(this)it==当前对象闭包不为null
withfun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()this==当前对象闭包调用同一个类的多个方法
可以省去类名,直接调用类的方法
runfun <T, R> T.run(block: T.() -> R): R = block()this==当前对象闭包let,with函数的任何场景
applyfun T.apply(block: T.() -> Unit): T { block(); return this }this==当前对象thisrun函数的任何场景,初始化实例的同时,直接操作属性并返回
动态inflate XML的View同时绑定数据
多个扩展函数链式调用
数据model多层级包裹判空处理的问题
alsofun T.also(block: (T) -> Unit): T { block(this); return this }it==当前对象this适用于let函数的任何场景,一般可用于多个扩展函数链式调用

观察者注册

  • 首先在最外部的setContent()方法之中调用了GlobalSnapshotManager.ensureStarted()方法
internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
)
: Composition {
    GlobalSnapshotManager.ensureStarted()
    ....
}
  • ensureStarted其内部注册了一个全局的globalWriteObserver
fun ensureStarted() {
    if (started.compareAndSet(falsetrue)) {
        removeWriteObserver = Snapshot.registerGlobalWriteObserver(globalWriteObserver)
    }
}

started是AtomicBoolean,其本质是使用了CPU的CAS指令保证原子性,由于是CPU级别的指令,其开销比需要操作系统参与的锁要小。

  • 接下来我们看下globalWriteObserver的实现
private val globalWriteObserver: (Any) -> Unit = {
    if (!commitPending) {
        commitPending = true
        schedule {
            commitPending = false
            Snapshot.sendApplyNotifications()
        }
    }
}
  • Compose 其实忽略了多次schedule,其内部将CallBackList作为monitor lock,最终同步执行其invoke(未完成的)并进入更新状态。
private fun schedule(block: () -> Unit) {
     synchronized(scheduledCallbacks) {
         scheduledCallbacks.add(block)
         if (!isSynchronizeScheduled) {
             isSynchronizeScheduled = true
             scheduleScope.launch { synchronize() }
         }
     }
 }
 private fun synchronize() {
         synchronized(scheduledCallbacks) {
             scheduledCallbacks.forEach { it.invoke() }
             scheduledCallbacks.clear()
             isSynchronizeScheduled = false
         }
 }
```---

- 最终会调用Snapshot.sendApplyNotifications()

```kotlin
fun sendApplyNotifications() {
    val changes = sync {
        currentGlobalSnapshot.modified?.isNotEmpty() == true
    }
    if (changes)
        advanceGlobalSnapshot()
}

此方法的modified也是Compse的核心之一,是实现扩展特性的重点,下文有解析

  • 当modified不为空,则调用advanceGlobalSnapshot
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    val previousGlobalSnapshot = currentGlobalSnapshot
    val result = sync {
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        val observers = sync { applyObservers.toList() }
        for (observer in observers) {
            observer(modified, previousGlobalSnapshot)
        }
    }
  .....
    return result
}

此方法会将发生状态修改的值,通知给observer,最终上文的notifyWrite方法就会通知给Compose,完成UI驱动

接下来我们分析具体有哪些回调

重组

这里源码篇幅很大,我直接上图做出总结

  • 每当状态更新时,都会发生重组。Composable函数必须明确获知新状态,才能相应地进行更新。
  • 上文提出的applyObservers中其实包含了两个观察者,结论如下:
    • SnapshotStateObserver.applyObserver:用于更新Snapshot
    • recompositionRunner:处理重组过程

绘制

  • 此时Compose完成了View树的创建,并且包含了承载的数据,但是Compose的渲染是独立于创建
  1. Composable函数不一定仅在主线程运行
  2. 重组是乐观操作,如果数据驱动事件在重组完成前完成,将会取消本次重组,所以编写时应该保证其结果幂等

渲染

  • 渲染最终都是调用ReusableComposeNode()方法,创建LayoutNode作为View节点

LayoutNode其实有点类似于Flutter的Element,共同组成了View树

  • AndroidComposeView是Compose的底层依赖,其内部拥有CanvasHolder

    这里的结论只是总结性的结论,很多具体的元素的实现还很多区别,但是其本质都是Canvas代理

    另外一点是Compose是独立于平台的【跨平台】,其实也是为了更高的平台兼容性

    • CanvasHolder 将 android.graphics.Canvas 代理成 androidx.compose.ui.graphics.Canvas,最终传递至LayoutNode 用于各种绘制

固有特性测量

  • 区别于Android传统UI系统的多次测量,Compose只测量一次
  • 如果需要依托于子View的测量信息,可以通过固有特性测量实现获取子view的固有特性测量信息,然后进行实际测量
    • (min|max)IntrinsicWidth:给定View的最小/最大宽度
    • (min|max)IntrinsicHeight:给定View的最小/最大高度
  • Android传统UI系统测量时间复杂度:O(2n)  n=View层级深度  2=父View对子View的测量次数
    • View层级增加,测量次数翻倍
  • 固有特性测量可以在父View测量之前,预先获取到每个子View的宽高信息,来计算自身的宽高
    • 有些场景下不希望父View参与计算,而希望子View间通过测量的先后顺序直接相互影响,可以使用SubcomposeLayout 来处理子View之间存在依赖关系的场景

总结

问题

  1. 动态展示UI:在执行过程之中不断的变化UI,就需要不断的对其进行验证,并且确保其依赖关系。还要保证生命周期内的依赖满足情况。
  2. 紧耦合:一处代码影响多处,并且大概率情况是隐式,看起来毫无关联,实际有影响
  3. 命令式UI:在编写UI代码时,随时都要考虑如何转换到对应的状态
  4. 单继承:是否可以通过其他方式实现突破单继承的限制?
  5. 代码膨胀:随着业务的不断扩展,如何控制代码膨胀?

关注点分离

从内聚和耦合去思考关注点分离 (Separation of concerns, SOC)

  • 耦合:不同模块中元素之间的依赖关系,元素之间互相影响
    • 紧耦合:一处代码影响多处,并且大概率情况是隐式,看起来毫无关联,实际有影响
  • 内聚:一个模块中各个元素之间的关系,模块之中各个元素相互组合的合理程度
    • 尽可能的将相关的代码组合在一起,随着代码规模的增长,去扩展代码

Composable函数

  • Composable 函数可以成为数据的转换函数。可以使用任何 Kotlin 代码来获取这一数据,并利用它来描述层级结构
  • 当调用了其他 Composable 函数,这些调用代表了我们层次结构中的 UI。并可以使用 Kotlin 中语言级别的原语来动态执行各种操作。
    例子: 可以使用 if 语句,for 循环来实现控制流,来处理 UI 逻辑。
  • 利用Kotlin 的尾随 lambda 语法,实现 Composable lambda 参数的 Composable 函数,即Composable函数指针

声明式UI

  • 编写代码时是按想法直接描述 UI,而不是如何转换到对应的状态。
  • 不需要关注 UI 在先前是什么状态,而只需要指定当前应当处于的状态。
  • Compose控制着如何从一个状态转到其他状态,所以不再需要考虑状态转换。

封装

Composable 函数可以管理和创建状态,然后将该状态及它接收到的任何数据作为参数传递给其他的 Composable 函数。

重组

无论Composable结构多么庞大,Composable函数都可以在任何时候被重新调用重组。根据其特性,当其中的某一部分发生变化时,Composable函数并不会重新计算整个层级结构

observeAsState

  • observeAsState 方法会把 LiveData 映射为 State,在函数体的Scope使用其值。
  • State 实例订阅了 LiveData 实例, State 会在 LiveData 发生改变的任何地方更新。
  • 无论在何处读取 State 实例,还是已经被包括的代码、还是已被读取的 Composable 函数都会自动订阅这些改变。
  • 不再需要指定 LifecycleOwner 或者更新回调,Composable 可以隐式地实现这两者的功能。

组合

与传统的继承不同,组合可以把一系列简单代码组合成一段复杂的代码逻辑,突破单继承的限制。利用Kotlin 的尾随 lambda 语法,实现 Composable lambda 参数的 Composable 函数,即Composable函数指针。


- FIN -


 推荐阅读
Compose正式发布,来打造一个Flappy Bird!
使用Jetpack Compose完成你的自定义Layout
使用Jetpack Compose完成自定义手势处理
Jetpack Compose 1.0 正式发布,Android 声明式 UI 时代已经到来!



Compose 博物馆网站:https://compose.net.cn/


添加微信进入 Compose 技术交流群



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存