JetBrains Compose Multiplatform 底层原理

171 阅读6分钟

今天我们来深入探讨 Compose Multiplatform(特别是 JetBrains Compose 和 JetBrains 主导的 Kotlin Multiplatform 版本)的底层原理。

首先需要明确一点:当我们谈论“Compose 多平台”时,通常指的是两个项目:

  1. JetBrains Compose Multiplatform: 最初由 JetBrains 开发,专注于 Desktop 和 Web,后来通过 KMM 扩展到 iOS。这是本文讨论的重点。
  2. 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 描述,并与之前的描述进行智能对比,最终只更新需要变化的部分。
  • 状态驱动:UI 只是状态的视觉映射。State 变化是重组发生的唯一原因。这极大地简化了 UI 与数据的同步逻辑。


2. 架构分层

Compose Multiplatform 的架构可以看作自上而下的三层:

层 1:公共的 Kotlin 代码层 (Compose Compiler & Runtime)

这是所有平台共享的核心逻辑,也是 Compose 魔法发生的地方。

  • Compose Compiler (Kotlin Compiler Plugin)

    • 它在编译期介入,分析和转换你的 @Composable 函数。
    • 关键作用
      1. 插入“重组触发器”:它修改你的函数字节码,注入逻辑来跟踪函数体内读取了哪些状态(State/MutableState)。当这些状态变化时,就知道需要重组哪个函数。
      2. 构建“调用树”:它将你的 UI 描述转换为一棵可以在重组时被比较和更新的树状数据结构(Composition)。它理解 remember, SideEffect 等概念的语义。
      3. 优化与记忆:它启用强大的优化,如“跳过”(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。而是:
      1. Compose 将 UI 树计算好布局和绘制指令。
      2. 通过一个 Skia 到 CoreGraphics 的转换层,将这些绘制指令转换为 iOS 的 CoreGraphics API 调用。
      3. 最终,整个 UI 被绘制到一个巨大的 CAMetalLayer(Metal)上,并嵌入到唯一的 UIViewController 中。输入事件也从 iOS 系统接收并转发给 Compose 处理。

层 3:原生平台层

这是 Compose 最终运行的环境。

  • JVM: 一个原生的应用程序窗口。
  • 浏览器: 一个浏览器标签页。
  • iOS: 一个原生的 UIViewController

3. 核心工作流程(以 Desktop 为例)

  1. 初始化: 应用启动,调用 setContent { MyApp() }
  2. 首次组合
    • MyApp 可组合函数被首次执行。
    • Compose Runtime 在内存中构建一棵 UI 树(Composition),记录所有需要绘制的元素和它们的状态依赖关系。
  3. 布局与绘制
    • Compose UI 层遍历这棵树,进行测量(Measure)和布局(Layout)。
    • 最终,平台层(Skia)接收到一系列详细的绘制命令(Draw Command),并将像素绘制到窗口上。
  4. 状态改变与重组
    • 用户点击按钮,修改了一个 MutableState 的值。
    • Compose Runtime 被通知该状态已变化。它查找所有读取了该状态的 @Composable 函数,并标记它们需要重组。
  5. 智能差异比较
    • Runtime 重新执行那些被标记的函数,生成一棵新的 UI 树。
    • 将新树与旧树进行对比(Diffing),计算出最小变更集(例如,只是某个文本的颜色和内容变了)。
  6. 更新
    • 只有发生变化的部分才会被重新布局和绘制。Skia 引擎会高效地只更新屏幕的那一小块区域。

总结与优势

特性原理优势
声明式UI用函数描述状态对应的UI,状态变则函数重组。代码更简洁、不易出错、易于状态管理。
编译器魔法Kotlin 编译器插件在编译时注入跟踪和优化代码。实现高效的重组,智能跳过不必要的函数执行。
解耦架构分层设计:通用逻辑层 + 平台适配层。实现了真正的共享UI逻辑,而不仅仅是业务逻辑。
自绘引擎主要平台(Desktop, Android, iOS)上使用 Skia 或等效物直接绘制。极致的外观一致性 across platforms,不受原生控件限制,性能可控。
虚拟DOM (Web)在Web端采用类似React的虚拟DOM差异比较策略。在保证开发模型一致的同时,获得Web上的高性能。

底层原理的核心在于:Compose Compiler 和 Runtime 提供了一个与平台无关的、用于构建和更新UI树的状态管理机制,而平台层则负责用最高效的方式将这颗UI树渲染到目标屏幕上。 这种设计使得开发者可以用同一套 Kotlin 代码和声明式思维模型,构建真正跨平台的本地应用程序。