探索 Jetpack Compose(一)

前言

在大前端概念快速发展下,出现了很多声明式 UI 写法的语言或框架,像前端的 react、iOS 的 Swift UI,还有 Google 的 Flutter,但很少会听到 Android 原生有什么革命性的声明式 UI 的技术或支持跨平台,这不,Google 推出了 Compose。下面,文中会对 Compose 做一些简要的剖析,以及带大家简单认识一下 KMM 的神秘面纱。

Compose 是什么?

发展路线

  • 2019 年 Google I/O 大会上公布的新的 UI 库 Compose,Jetpack 中的一个新成员
  • 2021 年 7 月,Compose 终于迎来 1.0 正式版
  • 2022 年 2 月,Compose 发布 1.1 正式版

简介

官方:更少的代码、直观、加速开发、功能强大 声明式 UI;更简单的自定义;实时的、带交互的预览功能;还有更强的性能和功能。

AndroidView 与 Compose 写法对比

AndroidView 写法

普遍写在 xml,大多数命令式 UI,代码去指挥、去命令界面更新,但结合 DataBinding 能做到声明式,写的时候要注意层级,view 树 measure 多次测量耗时

既生瑜何生亮?

Compose 写法

声明式 UI,通过订阅机制可以自动的刷新 UI,并且是 diff,层级随便套,Compose 禁用了二次测量,但加入了一个新东西:Intrinsic Measurement,官方把它翻译做「固有特性测量」

所谓的 Intrinsic Measurement,指的是 Compose 允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」,直白地说就是「你内部内容的最大或者最小尺寸是多少」


xml 的写法进行多次测量时,以每个子 view 需要二次测量为例 旧的测量复杂度.png 每个 View 的测量算法的时间复杂度是 O(2ⁿ)

而 Intrinsic Measurement 是怎样做的呢

compose测量复杂度.png Compose 会先对整个组件树进行一次 Intrinsic 测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。复杂度从 O(2ⁿ) 降到了 O(n)

Compose 怎么用?

api 太多,不一一示例,力求举一反三,以点到面


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeStudyTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
                    Greeting()
                }
            }
        }
    }
}

@Composable
fun Greeting() {
    var addCount by remember { mutableStateOf(0) }
    Column {
        Text(
            text = "Hello World $addCount",
            modifier = Modifier.clickable { addCount += 1 }
        )
        for (i in 0 until addCount) {
            HelloWord(text = "我是第 ${i + 1} 个 动态 add 的 Text")
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeStudyTheme {
        Greeting()
    }
}
复制代码

compose demo.png

Compose 简要原理

从上面的 demo 我们看到 ColumnText,还看到了 @Composableremember 等,那我们深入分析下

@Composable

Compose 没有使用注解处理器,Compose 在 Kotlin 编译插件在类型检查和代码生成阶段工作,因此不需要使用注解处理器。这个注解更像是一个关键字,就像协程的 suspend 关键字一样,添加了 @Composable 注解时,就改变了这个函数类型,相同函数类型没有注解和有注解是不兼容的。

@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), null, null, null, TextUnit.m2481constructorimpl(0), null, null, TextUnit.m2481constructorimpl(0), null, false, 0, null, null, $composer2, i & 14, 0, 65534);
        } else {
            $composer3.skipToGroupEnd();
            $composer2 = $composer3;
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup != null) {
            endRestartGroup.updateScope(new HelloKt$HelloWord$1(text, $changed));
        }
    }
复制代码

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

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

Gap具有移动到任何区域的能力,所以在数据插入与删除时更高效。 Slot Table 其本质是一个线性的数据结构,所以支持将 View 树存储在 Slot Table 中。 根据 Slot Table 可移动插入点的特性,让View树在变动之后无需重新创建整个View树的数据结构。

remeber

remeber 是一个 Composable 函数,内部实现类似于委托,实现了 Composable 函数调用链中的对象记忆。 Composable 函数在调用链位置不变的情况下,调用 remember 即可获取上次调用时记忆的内容

  • 同一个 Composable 函数在不同位置被调用,其 remember 函数获取的内容也不同
  • 同一个 Composable 函数被多次调用,将会产生多个实例。每次调用都有其自己的生命周期

mutableStateOf

mutableStateOf 真正的内部实现是 SnapshotMutableStateImpl

fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)

internal actual fun <T> createSnapshotMutableState(
    value: T,
    policy: SnapshotMutationPolicy<T>
): SnapshotMutableState<T> = ParcelableSnapshotMutableState(value, policy)

internal class ParcelableSnapshotMutableState<T>(
    value: T,
    policy: SnapshotMutationPolicy<T>
) : SnapshotMutableStateImpl<T>(value, policy), Parcelable {
    ...
}

internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if (!policy.equivalent(it.value, value)) {
                next.overwritable(this, it) { this.value = value }
            }
        }
}
复制代码

在 SnapshotMutableStateImpl value 的 set 方法之中,其完成了对观察者的通知

Snapshot.kt

internal inline fun <T : StateRecord, R> T.overwritable(
    state: StateObject,
    candidate: T,
    block: T.() -> R
): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.overwritableRecord(state, snapshot, candidate).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}

internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}
复制代码

Snapshot.current获取当前的Snapshot,具体场景分析如下:

  • 如果通过异步操作更新,因为 Snapshot 是一个 ThreadLocal ,所以会返回当前执行线程的 Snapshot
  • 如果当前执行线程的 Snapshot 为空时默认返回 GlobalSnapshot
  • 如果在Composable中直接对 mutableState 进行更新操作,当前 Composable 执行线程的 Snapshot 就是 MutableSnapshot

最终调用 notifyWrite 方法完成观察者的通知

那么问题来了,是在什么时候进行观察者的订阅呢? 我们看回 setContent 函数,经过层层调用会调到 ViewGroup.setContent

internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    ....
}

internal object GlobalSnapshotManager {
    fun ensureStarted() {
        ...
        Snapshot.registerGlobalWriteObserver {
                channel.trySend(Unit)
        }
    }
}
复制代码

Snapshot.kt

fun registerGlobalWriteObserver(observer: ((Any) -> Unit)): ObserverHandle {
            sync {
                globalWriteObservers.add(observer)
            }
            advanceGlobalSnapshot()
            return ObserverHandle {
                sync {
                    globalWriteObservers.remove(observer)
                }
                advanceGlobalSnapshot()
            }
}

private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    val previousGlobalSnapshot = currentGlobalSnapshot.get()
    val result = sync {
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    // If the previous global snapshot had any modified states then notify the registered apply
    // observers.
    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
        observers.fastForEach { observer ->
            observer(modified, previousGlobalSnapshot)
        }
    }

    return result
}
复制代码

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

ComposeView

Compose 在渲染时并不会转化成View,而是只有一个入口 View,即 AndroidComposeView 我们声明的 Compose 布局在渲染时会转化成 NodeTree, AndroidComposeView 中会触发 NodeTree 的布局与绘制 总得来说,Compose 会有一个 View 的入口,但它的布局与渲染还是在 LayoutNode 上完成的,基本脱离了 View

Compose 树.png

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    //判断ComposeView是否存在,如果存在则不创建
    if (existingComposeView != null) with(existingComposeView) {
        setContent(content)
    } else ComposeView(this).apply {
        //将Compose content添加到ComposeView上
        setContent(content)
        // 将ComposeView添加到DecorView上
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

复制代码

在 setContent 的过程中,会创建 ComposeView 与 AndroidComposeView,其中AndroidComposeView 是 Compose 的入口,并添加到DecorView上 AndroidComposeView 在 dispatchDraw 中会通过 root 向下遍历子节点进行测量布局与绘制,这里是 LayoutNode 绘制的入口 在Android平台上,Compose 的布局与绘制已基本脱离 View 体系,但仍然依赖于 Canvas

AndroidComposeView.kt

override fun dispatchDraw(canvas: android.graphics.Canvas) {
        // Compose测量与布局入口
        measureAndLayout()
        
        // Compose绘制入口
        canvasHolder.drawInto(canvas) { root.draw(this) }
        ...
    }

    override fun measureAndLayout() {
        val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
        measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
    }
复制代码

AndroidComposeView 会通过 root,向下遍历它的子节点进行测量布局与绘制,这里就是 LayoutNode 绘制的入口

什么是 KMM

在说 KMM 之前,我们先来了解一下 Compose Multiplatform

Compose Multiplatform

  • 2021 年 12 月,JetBrains 发布了 Compose Multiplatform 1.0 正式版

Compose Multiplatform 可以看做是 Jetpack Compose 的超集,在 Jetpack Compose 的基础上扩展出了跨平台的能力,两者共享了大部分的核心公共 API,所以 Compose Multiplatform 的很多基础库均还是以 androidx.compose.xxx 作为包名,这使得已经通过 Jetpack Compose 实现的 Android 应用可以比较方便地移植到其它平台,两者具有完美的互操作性。

Compose Multiplatform 支持市面上的大部分主流平台

支持平台.png

支持平台.png @ 图源 INFOQ 微信公众号

Compose Multiplatform 通过用不同的编译器编译同一份代码来生成各端的不同产物,从而达到跨平台的目的,最终的编译产物和目标平台完全相容。例如,通过 Kotlin/JVM 为 JVM 和 Android 平台生成 jar/aar 文件、通过 Kotlin/Native 为 IOS 平台生成 framework 文件、通过 Kotlin/JS 为 Web 平台生成 JavaScript 文件,最终调用的还是原生 API,这使得采用 Compose Multiplatform 不会导致性能损耗,且不会像 Flutter 那样明显增大应用体积。

Kotlin Multiplatform Mobile(KMM)

Compose Multiplatform 在移动端的跨平台框架子集叫做 Kotlin Multiplatform Mobile(KMM)。 和 Flutter 不同,KMM 并不追求在各个平台使用一套完全相同的 UI 和 代码,没有像 Flutter 那样内置一个统一的图型绘制引擎,因此 KMM 虽然支持多端复用 UI,但功能还比较弱,在构建 UI 时还是需要依赖于平台 API。KMM 侧重于在 UI 层以下共享一套适用于所有平台的通用业务逻辑,统一通过 Kotlin 来编写业务代码,并同时保持和原生开发语言(Java、Objective-C、Swift、JavaScript 等)之间的互通性,具备灵活性的同时也保留了原生编程的优势。

后话

思考点:

  1. 业务选择 Compose 时应该怎样循序过渡?

支持原生 AndroidView 和 Compose 混用

  1. 目前 Compose 还有很长的路发展,该不该学和用?

什么是未来?我不知道,但我知道,谷歌推过的都逐渐成为了开发主流

  1. 对中台思考
  • KMM 能解决两端统一性问题,人力资源问题
  • 往往来说直播中台更侧重于公共逻辑层,业务怎样写 view 不太关心但需要都能兼容好
  1. Compose 应该搭配什么架构开发?
  • MVVM、MVI?

“没有最好的架构,只有最合适的架构”

参考

猜你喜欢

转载自juejin.im/post/7076632712185905189