今天我们来深入探讨 Compose Multiplatform(特别是 JetBrains Compose 和 JetBrains 主导的 Kotlin Multiplatform 版本)的底层原理。
首先需要明确一点:当我们谈论“Compose 多平台”时,通常指的是两个项目:
- JetBrains Compose Multiplatform: 最初由 JetBrains 开发,专注于 Desktop 和 Web,后来通过 KMM 扩展到 iOS。这是本文讨论的重点。
- Android Jetpack Compose: 由 Google 开发,专注于 Android。
它们的核心思想、声明式 UI 范式和解耦理念是相通的,但底层实现和部分架构因目标平台而异。
Compose Multiplatform 的底层原理可以分解为几个核心层次来理解:
1. 核心思想:声明式 UI 与状态驱动
这是所有 Compose 技术的基石,与 React、Flutter、SwiftUI 等现代 UI 框架一致。
-
命令式 vs 声明式:
- 命令式(传统 Android View,Swing):你通过编写详细的步骤来告诉 UI如何更新(例如,
findViewById<TextView>(R.id.text).setText(“new text”))。 - 声明式(Compose):你通过可组合函数描述UI 在特定状态下的样子(例如,
Text(text = state.value))。当状态(state.value)改变时,框架会自动重新调用(重组) 相关的可组合函数,生成新的 UI 描述,并与之前的描述进行智能对比,最终只更新需要变化的部分。
- 命令式(传统 Android View,Swing):你通过编写详细的步骤来告诉 UI如何更新(例如,
-
状态驱动:UI 只是状态的视觉映射。
State变化是重组发生的唯一原因。这极大地简化了 UI 与数据的同步逻辑。
2. 架构分层
Compose Multiplatform 的架构可以看作自上而下的三层:
层 1:公共的 Kotlin 代码层 (Compose Compiler & Runtime)
这是所有平台共享的核心逻辑,也是 Compose 魔法发生的地方。
-
Compose Compiler (Kotlin Compiler Plugin):
- 它在编译期介入,分析和转换你的
@Composable函数。 - 关键作用:
- 插入“重组触发器”:它修改你的函数字节码,注入逻辑来跟踪函数体内读取了哪些状态(
State/MutableState)。当这些状态变化时,就知道需要重组哪个函数。 - 构建“调用树”:它将你的 UI 描述转换为一棵可以在重组时被比较和更新的树状数据结构(
Composition)。它理解remember,SideEffect等概念的语义。 - 优化与记忆:它启用强大的优化,如“跳过”(Skip),如果重组时函数的参数(尤其是稳定类型的参数)没有变化,编译器可以安排完全跳过该函数的执行,极大提升性能。
- 插入“重组触发器”:它修改你的函数字节码,注入逻辑来跟踪函数体内读取了哪些状态(
- 它在编译期介入,分析和转换你的
-
Compose Runtime:
- 这是 Compose 的大脑,在所有平台上通用。它管理着 UI 的“状态”和“描述”。
- 关键概念:
- Composition: 内存中维护的 UI 树结构,是 Composable 函数执行结果的表示。
- Recomposition: 当状态变化时,Runtime 会调度并执行重组过程。它智能地比较新旧两棵“树”(Diffing),计算出最小变更集。
- State Management: 管理
MutableState等状态对象,并在它们被修改时通知 Runtime 触发重组。
这一层是平台无关的,只处理逻辑和描述,不涉及任何实际的绘图或控件操作。
层 2:平台特定的适配层 (Compose UI Layer)
这一层将公共的、平台无关的 UI 描述(“画一个红色的矩形”)翻译成各个平台能理解的指令。这是多平台能力的关键。
- Compose UI: 定义了一套公共的、平台无关的绘图和布局 API。例如
Canvas,Layout,DrawScope,MeasureScope等。你的所有可组合组件(如Box,Column,Text)最终都基于这些原始API构建。 - 平台实现:
- Desktop (JVM): 实现为一个高性能的自绘引擎。它直接使用
Skia(一个强大的 2D 图形库)或Java2D(在 macOS 上也可使用 Metal)在JFrame上直接进行光栅化和绘制。它自己处理输入事件、布局和渲染,不依赖原生桌面控件。这带来了极高的UI一致性和灵活性。 - Web (JS): 将 Composable 树翻译成 DOM 节点和一个虚拟 DOM(用于高效的 Diffing 和更新)。
Layout被转换为 CSS Flexbox/Grid,Canvas被转换为<canvas>元素。它生成的是标准的 HTML/CSS/JS。 - Android: 实际上,JetBrains Compose for Android 与 Jetpack Compose 共享了大量理念,但为了在 KMM 中共享代码,它提供了一个兼容层,使得为 JetBrains Compose 编写的代码可以在 Android 上运行。其底层仍然是高效的 Skia 渲染。
- iOS (通过 KMM): 这是最巧妙的部分。Compose 并没有在 iOS 上直接使用 Skia。而是:
- Compose 将 UI 树计算好布局和绘制指令。
- 通过一个 Skia 到 CoreGraphics 的转换层,将这些绘制指令转换为 iOS 的
CoreGraphicsAPI 调用。 - 最终,整个 UI 被绘制到一个巨大的
CAMetalLayer(Metal)上,并嵌入到唯一的UIViewController中。输入事件也从 iOS 系统接收并转发给 Compose 处理。
- Desktop (JVM): 实现为一个高性能的自绘引擎。它直接使用
层 3:原生平台层
这是 Compose 最终运行的环境。
- JVM: 一个原生的应用程序窗口。
- 浏览器: 一个浏览器标签页。
- iOS: 一个原生的
UIViewController。
3. 核心工作流程(以 Desktop 为例)
- 初始化: 应用启动,调用
setContent { MyApp() }。 - 首次组合:
MyApp可组合函数被首次执行。- Compose Runtime 在内存中构建一棵 UI 树(Composition),记录所有需要绘制的元素和它们的状态依赖关系。
- 布局与绘制:
- Compose UI 层遍历这棵树,进行测量(Measure)和布局(Layout)。
- 最终,平台层(Skia)接收到一系列详细的绘制命令(Draw Command),并将像素绘制到窗口上。
- 状态改变与重组:
- 用户点击按钮,修改了一个
MutableState的值。 - Compose Runtime 被通知该状态已变化。它查找所有读取了该状态的
@Composable函数,并标记它们需要重组。
- 用户点击按钮,修改了一个
- 智能差异比较:
- Runtime 重新执行那些被标记的函数,生成一棵新的 UI 树。
- 将新树与旧树进行对比(Diffing),计算出最小变更集(例如,只是某个文本的颜色和内容变了)。
- 更新:
- 只有发生变化的部分才会被重新布局和绘制。Skia 引擎会高效地只更新屏幕的那一小块区域。
总结与优势
| 特性 | 原理 | 优势 |
|---|---|---|
| 声明式UI | 用函数描述状态对应的UI,状态变则函数重组。 | 代码更简洁、不易出错、易于状态管理。 |
| 编译器魔法 | Kotlin 编译器插件在编译时注入跟踪和优化代码。 | 实现高效的重组,智能跳过不必要的函数执行。 |
| 解耦架构 | 分层设计:通用逻辑层 + 平台适配层。 | 实现了真正的共享UI逻辑,而不仅仅是业务逻辑。 |
| 自绘引擎 | 主要平台(Desktop, Android, iOS)上使用 Skia 或等效物直接绘制。 | 极致的外观一致性 across platforms,不受原生控件限制,性能可控。 |
| 虚拟DOM (Web) | 在Web端采用类似React的虚拟DOM差异比较策略。 | 在保证开发模型一致的同时,获得Web上的高性能。 |
底层原理的核心在于:Compose Compiler 和 Runtime 提供了一个与平台无关的、用于构建和更新UI树的状态管理机制,而平台层则负责用最高效的方式将这颗UI树渲染到目标屏幕上。 这种设计使得开发者可以用同一套 Kotlin 代码和声明式思维模型,构建真正跨平台的本地应用程序。