主题
第 7 章 跨端架构原理 — KMP + 两棵树 + Bridge
学习目标:搞清楚一份 Kotlin 代码是怎么变成 6 端原生产物的;理解 Kuikly「两棵树架构」的设计思想;摸清「渲染指令」是如何从 Kotlin 层流到原生层的;掌握"为什么 Kuikly 比 RN 快、比 Flutter 轻"的本质答案。
7.1 架构鸟瞰图
┌────────────────────────────────────────────────────────────────────┐
│ Kuikly 整体架构(6 端通用) │
├────────────────────────────────────────────────────────────────────┤
│ │
│ 你的业务代码 │
│ ───────────── │
│ @Page("Home") │
│ class HomePage : Pager() { ... } ← Kotlin commonMain │
│ │ │
│ │ KMP 编译 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Kuikly 跨端核心层(commonMain) │ │
│ │ ──────────────────────────────────── │ │
│ │ - DSL 解析 → BuildTree(原型树) │ │
│ │ - Diff → RenderTree(渲染树) │ │
│ │ - 测量、布局(Yoga / Flexbox) │ │
│ │ - 响应式调度(observable) │ │
│ │ - 渲染指令生成(CreateView / SetProp / ...) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 渲染指令 (JSON-like) 通过 Bridge 下发 │
│ ▼ │
│ ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │
│ │ Android │ iOS │ HarmonyOS│ Web │ 小程序 │ macOS │ │
│ │ 原生层 │ 原生层 │ 原生层 │ 原生层 │ 原生层 │ 原生层 │ │
│ │ │ │ │ │ │ │ │
│ │ View │ UIView │ ArkUI │ DOM │ WXML │ NSView │ │
│ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘📌 三层心智:
- 业务层:你写的
@PageKotlin 类- 跨端核心层:Kuikly 框架自己(90% 用 Kotlin commonMain 写)
- 原生渲染层:每个平台一份「轻量的桥接 + 控件适配器」,10% 用平台原生语言
7.2 KMP 编译:一份代码 → 6 端产物
7.2.1 KMP 是什么
KMP(Kotlin Multiplatform) 是 JetBrains 推出的多平台编译能力。同一份 Kotlin 代码可以编译到不同目标:
┌─────────────────────────────────────────────────────────┐
│ KMP 目标平台 → 编译产物 │
├─────────────────────────────────────────────────────────┤
│ │
│ Kotlin/JVM → .class / .jar / .aar (Android, JVM) │
│ Kotlin/Native → .framework / .dylib (iOS, macOS) │
│ → .so (HarmonyOS, Linux) │
│ → .a (静态库) │
│ Kotlin/JS → .js (Web, 小程序) │
│ Kotlin/Wasm → .wasm (实验中) │
│ │
└─────────────────────────────────────────────────────────┘7.2.2 「commonMain + actual」的源码集结构
KMP 工程的源码组织方式:
shared/
└── src/
├── commonMain/ ← 跨端共享代码(90%)
│ └── kotlin/
│ └── ...
│
├── androidMain/ ← Android 平台特有(10%)
├── iosMain/ ← iOS 平台特有
├── ohosArm64Main/ ← HarmonyOS 平台特有
├── jsMain/ ← Web 平台特有
│
└── commonTest/ ← 跨端测试7.2.3 expect / actual 机制:写跨端 API 的标准范式
如果某个 API 需要平台差异化实现,用 expect 在 commonMain 声明,在每个 platformMain 里用 actual 实现:
kotlin
// commonMain
expect fun currentPlatform(): String
// androidMain
actual fun currentPlatform(): String = "Android ${android.os.Build.VERSION.RELEASE}"
// iosMain
actual fun currentPlatform(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
// jsMain
actual fun currentPlatform(): String = "Web ${js("navigator.userAgent")}"调用方写法跟普通函数没区别:
kotlin
// commonMain
val platform = currentPlatform()
println("Hello from $platform")7.2.4 编译产物对照
| 平台 | KMP 目标 | 编译产物 | 集成方式 |
|---|---|---|---|
| Android | jvm | .aar | Gradle 依赖 |
| iOS | native (arm64/x64) | .framework | CocoaPods / SPM |
| HarmonyOS | native (arm64) | .so | hvigor |
| Web | js (IR) | .js (UMD) | npm / CDN |
| 小程序 | js | .js | 小程序构建 |
| macOS | native (arm64/x64) | .framework | SPM |
7.3 两棵树:BuildTree + RenderTree
7.3.1 为啥要"两棵树"?
回顾第 1 章:传统跨端框架(RN / Flutter)走「虚拟 DOM 三棵树」:
业务 DSL → Virtual Tree → Diff → Element Tree → RenderObject Tree → 平台原生
(3 棵 + 2 次 diff,开销不小)Kuikly 优化为「两棵树」:
业务 DSL ─→ BuildTree ─diff─→ RenderTree ─→ 原生指令
↑ ↑
Kotlin 层(90%) Kotlin 层算,再桥到 Native(10%)7.3.2 BuildTree(原型树)
执行 ViewBuilder 闭包后,得到的"组件原型描述"。它包含 所有节点,包括纯逻辑节点(如 vfor、vif 这种"指令节点")。
View {
attr { allCenter() }
Text { attr { text("A") } }
if (showB) Text { attr { text("B") } }
vfor({ list }) { Text { ... } }
}
↓↓↓
BuildTree:
View
├── Text "A"
├── ConditionNode (showB) ── Text "B" ← 逻辑节点
└── ForNode (list) ── [Text x N] ← 逻辑节点7.3.3 RenderTree(渲染树)
从 BuildTree 中剔除所有逻辑节点,只留下"会真正变成原生控件"的节点:
RenderTree:
View
├── Text "A"
├── Text "B" (showB=true 时)
├── Text item1
├── Text item2
└── Text item3RenderTree 跟原生控件1:1 对应。Kuikly 的 diff 算法只在这棵树上做,更轻量。
7.3.4 Diff 算法:精细化更新
当 observable 字段变化 → 重跑相关 attr 块 → 生成新的 BuildTree → 与旧的 BuildTree 做 diff → 把差异编译成「渲染指令」。
精细化指什么?看下面对比:
场景:count 从 0 → 1,影响一个 Text 的 text 属性
┌───────────────────────────────────────────────────────┐
│ RN:重渲染整个组件树 + 比较 props │
│ ↓ │
│ 再决定:Text 的 props.text 变了 → 调原生 setText │
├───────────────────────────────────────────────────────┤
│ Kuikly:直接知道「count 字段被这个 attr 块用了」 │
│ ↓ │
│ 只重跑这个 attr 块 → setProp("text", "1") 指令 │
│ │
│ ★ O(1) 精细化更新,不重建任何节点 │
└───────────────────────────────────────────────────────┘7.4 渲染指令:跨端通信的"通用语言"
7.4.1 指令格式
Kuikly 把 UI 操作抽象成 6 类核心指令,序列化后通过 Bridge 发到原生层:
┌─────────────────────────────────────────────────────┐
│ Kuikly 渲染指令清单 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. CreateView(viewId, type, parentId) │
│ - 创建一个原生 View │
│ │
│ 2. SetProp(viewId, key, value) │
│ - 修改某个 View 的属性 │
│ │
│ 3. AddEventListener(viewId, event) │
│ - 给 View 注册事件监听 │
│ │
│ 4. RemoveEventListener(viewId, event) │
│ │
│ 5. AddSubview(parentId, childId, index) │
│ - 把 child 挂到 parent 的指定位置 │
│ │
│ 6. RemoveSubview(parentId, childId) │
│ - 把 child 从 parent 移除 │
│ │
└─────────────────────────────────────────────────────┘7.4.2 一次按钮点击的完整流程
假设业务代码:
kotlin
private var count by observable(0)
Text { attr { text("Count: $count") } }
Button {
event { click { ctx.count++ } }
Text { attr { text("+1") } }
}用户点击按钮 → 完整链路:
① 用户点击原生按钮 View
│
▼
② 原生层捕获 click 事件 → 通过 Bridge 上抛到 Kotlin 层
│
▼
③ Kotlin 层找到 viewId=2 的 click 监听器
│
▼
④ 执行 click { ctx.count++ }
│
▼
⑤ count 字段 setter 触发 → 加入待重跑队列
│
▼
⑥ 下一帧:重跑读了 count 的 attr 块
│
▼
⑦ 拿到新的 text = "Count: 1"
│
▼
⑧ Diff: 跟旧值 "Count: 0" 不同 → 生成指令
│ SetProp(viewId=1, key="text", value="Count: 1")
▼
⑨ 通过 Bridge 把指令发到原生层
│
▼
⑩ 原生层执行:textView1.setText("Count: 1")
│
▼
屏幕显示 "Count: 1"整个过程没有任何序列化 / 反序列化的开销(Kotlin/JVM 直接函数调用),没有 JS 桥。
7.5 Bridge:跨语言通信桥
7.5.1 各端 Bridge 的不同实现
每个平台的 Bridge 实现方式不同,但接口语义统一:
| 平台 | Bridge 实现 | 性能 |
|---|---|---|
| Android | JNI + 直接 JVM 调用(无桥) | 极快(普通函数调用) |
| iOS | Kotlin/Native 与 OC/Swift 互操作 | 极快(直接 ObjC 消息) |
| HarmonyOS | Kotlin/Native 与 ArkTS 桥 | 较快(NAPI 调用) |
| Web | Kotlin/JS 与 DOM 直调 | 快(同语言) |
| 小程序 | Kotlin/JS + 小程序 setData | 中(受小程序架构限制) |
7.5.2 Bridge 的"两个方向"
┌────────────────────────────────────────────────────┐
│ Kotlin 层 │
│ ↑ │ │
│ │ 上行:事件回调 │ 下行:渲染指令 │
│ │ click / scroll / ... │ CreateView ... │
│ │ ↓ │
│ 原生层(Android View / iOS UIView / ...) │
└────────────────────────────────────────────────────┘7.5.3 跟 RN Bridge 的本质差异
┌─────────────────────────────────────────────────────────────┐
│ RN Bridge(旧架构) │
├─────────────────────────────────────────────────────────────┤
│ JS 引擎 ─── 序列化 JSON ───→ 跨线程消息队列 ───→ 原生 │
│ ↑ 序列化开销大 ↑ 异步消息有延迟 ↑ 反序列化开销 │
│ 每次调用都"一来一回",频繁调用时累计延迟显著 │
├─────────────────────────────────────────────────────────────┤
│ Kuikly Bridge(Android) │
├─────────────────────────────────────────────────────────────┤
│ Kotlin/JVM 直接函数调用 ───→ Android View 操作 │
│ ↑ 跟普通 Kotlin 调用一样 │
│ 无序列化、无消息队列、无线程跳转 → 极致性能 │
└─────────────────────────────────────────────────────────────┘📌 关键洞察:Android 上 Kuikly 编出来的
.aar就是 JVM 字节码,跟你写的 Kotlin 代码本质相同。跑起来KuiklyView.callMethod(...)就是targetObject.callMethod(...),没有"桥"。
7.6 KSP(Kotlin Symbol Processing):编译期魔法
7.6.1 KSP 是什么
KSP 是 Google 出品的 Kotlin 注解处理器,在编译期扫描你的源码,生成额外的 Kotlin 代码。Kuikly 用它做了几件大事:
┌─────────────────────────────────────────────────────┐
│ Kuikly KSP 在编译期做了什么 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. 扫描所有 @Page("xxx") 注解 │
│ → 生成 PagerRegistry:name → factory 映射 │
│ │
│ 2. 扫描所有 @Component 注解 │
│ → 生成组件工厂 │
│ │
│ 3. 生成 KuiklyCoreEntry.triggerRegisterPages() │
│ → 一行代码注册全部页面 │
│ │
└─────────────────────────────────────────────────────┘7.6.2 生成代码长啥样
你写的:
kotlin
@Page("HomePage")
class HomePage : Pager() { ... }
@Page("DetailPage")
class DetailPage : Pager() { ... }KSP 在编译期生成:
kotlin
// 自动生成,你看不到,但确实存在
object KuiklyCoreEntry {
fun triggerRegisterPages() {
PagerRegistry.register("HomePage") { HomePage() }
PagerRegistry.register("DetailPage") { DetailPage() }
}
}宿主在 MainActivity.onCreate 里调一次 triggerRegisterPages(),所有页面就登记好了。
7.6.3 为啥用 KSP 而不是反射?
| 维度 | KSP(编译期生成) | 反射(运行时扫描) |
|---|---|---|
| 启动速度 | 快(直接函数调用) | 慢(要扫包) |
| 包体 | 小 | 大(要带 Reflect 库) |
| iOS 兼容 | ✅(Kotlin/Native 不支持完整反射) | ❌ |
| 错误时机 | 编译期(IDE 高亮) | 运行时(线上才暴露) |
📌 结论:KSP 是 KMP 项目的"标配",特别是 iOS / 鸿蒙这种 Kotlin/Native 平台,反射受限。
7.7 跟 Flutter 架构对比
┌──────────────────────────────────────────────────────────────┐
│ Flutter 架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 你的 Dart 代码 │
│ ↓ │
│ Flutter Engine(C++ 核心) │
│ ↓ │
│ Skia 自绘引擎(自己画所有像素) │
│ ↓ │
│ 各平台 Surface(OpenGL / Vulkan / Metal) │
│ ↓ │
│ 屏幕 │
│ │
│ ★ 不依赖原生 UI 系统 │
│ ★ 跨端渲染一致性强 │
│ ★ 引擎包体大(5MB+) │
│ ★ 原生组件需自己模拟(系统弹窗、键盘行为有差异) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Kuikly 架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 你的 Kotlin 代码 │
│ ↓ KMP 编译 │
│ Kuikly 跨端核心层(Kotlin commonMain) │
│ ↓ 渲染指令 │
│ 各端原生渲染层(Android View / iOS UIView / ArkUI / ...) │
│ ↓ │
│ 屏幕 │
│ │
│ ★ 复用各平台原生 UI 系统 │
│ ★ 跨端有"系统差异"但用户感觉自然 │
│ ★ 包体小(300KB-1MB) │
│ ★ 系统组件直接用,无需模拟 │
└──────────────────────────────────────────────────────────────┘7.8 一帧的"完整生命周期"
把所有概念串起来:
一帧时间内(16.67ms)发生了什么?
────────────────────────────────────────
t=0 用户操作(点击 / 滚动)
t=1 原生层捕获 → Bridge 上抛
t=2 Kotlin 事件回调执行
t=3 observable 字段变化 → setter 触发
t=4 待重跑队列被加入相关 attr 块
t=5 ── 进入下一帧 ──
t=6 批量重跑 attr 块 → 生成新 BuildTree 节点
t=7 Diff → 生成渲染指令列表
t=8 指令通过 Bridge 下发
t=9 原生层执行:setText / setBackground / ...
t=10 系统调度下一次 vsync 信号
t=16 屏幕刷新,用户看到新画面📌 整个过程在单帧时间内完成,60fps 跑得稳。
7.9 章末小结
★ 第 7 章架构知识图谱 ★
│
┌────────────────────┼─────────────────────┐
│ │ │
┌───▼─────┐ ┌──────▼──────┐ ┌───▼──────┐
│ KMP 编译│ │ 两棵树架构 │ │ Bridge │
├─────────┤ ├─────────────┤ ├──────────┤
│ common │ │ BuildTree │ │ Android: │
│ actual │ │ RenderTree │ │ JNI │
│ 6 端产物│ │ O(1) Diff │ │ iOS: │
│ KSP │ │ 精细化更新 │ │ ObjC 互操│
└─────────┘ └─────────────┘ │ Web/小程序│
│ │ 同语言 │
▼ └──────────┘
渲染指令清单
CreateView / SetProp / AddSub / ...🎤 7.10 章末面试题(10 道高频题)
Q1. KMP 是什么?跟 Kuikly 是什么关系?
答:KMP(Kotlin Multiplatform) 是 JetBrains 推出的 Kotlin 多平台编译能力,能把同一份 Kotlin 代码编译到 Android / iOS / Web / 桌面 / 鸿蒙等多平台。
Kuikly 基于 KMP 构建:业务代码用 KMP 编译共享,UI 用 Kuikly 的 DSL 写。可以理解为「KMP 解决了"业务逻辑跨端",Kuikly 解决了"UI 跨端"」。
Q2. 为什么 commonMain 不能直接调 Android API?
答:因为 commonMain 要被编译到所有平台。Android API(android.content.Context 等)只在 Android 平台存在,iOS / 鸿蒙编译时找不到符号会报错。
如果业务确实需要平台 API,KMP 提供 expect / actual 机制:commonMain 写 expect,各 platformMain 写 actual 实现。
Q3. Kuikly 的「两棵树」具体是哪两棵?
答:
- BuildTree(原型树):DSL 闭包执行后生成的所有节点,包含逻辑节点(vif、vfor 等)
- RenderTree(渲染树):从 BuildTree 剔除逻辑节点,只留下「与原生控件 1:1 对应」的节点
Diff 在 RenderTree 上做,避免了"逻辑节点"参与 diff 的浪费。
Q4. Kuikly 的 Diff 跟 React/Vue 有什么本质不同?
答:
- React/Vue:从根节点开始遍历整棵树,对比 props 决定是否更新
- Kuikly:通过响应式追踪知道「哪个 attr 块依赖了变化的字段」,只重跑那个 attr 块,不遍历整棵树
后者是 O(1) 精细化更新(更新次数和数据变化次数线性相关,与组件总数无关)。
Q5. Kuikly 的渲染指令有哪几类?
答:6 大核心指令:
- CreateView(viewId, type, parentId)
- SetProp(viewId, key, value)
- AddEventListener(viewId, event)
- RemoveEventListener(viewId, event)
- AddSubview(parentId, childId, index)
- RemoveSubview(parentId, childId)
所有 UI 操作都能用这 6 类组合表达,指令格式跨端统一,每个原生层只需要实现这 6 个 handler。
Q6. Kuikly 的 Bridge 跟 RN Bridge 有什么本质区别?
答:
- RN Bridge:JS 引擎 ↔ 原生,需要序列化 JSON + 跨线程消息,频繁调用时累积延迟显著
- Kuikly Bridge(Android):Kotlin/JVM 直接 JNI 调用,没有序列化、没有跨线程,跟普通 Kotlin 函数调用一样
- Kuikly Bridge(iOS):Kotlin/Native 直接调 OC,几乎零开销
性能上,Kuikly Bridge ≈ 原生调用,RN Bridge 慢 1-2 个数量级。
Q7. KSP 在 Kuikly 里干了什么?为什么不用反射?
答:KSP 在编译期:
- 扫描所有
@Page("xxx")→ 生成路由注册代码 - 扫描所有
@Component→ 生成组件工厂 - 生成
KuiklyCoreEntry.triggerRegisterPages()入口
不用反射的原因:
- iOS / 鸿蒙是 Kotlin/Native 平台,反射受限
- 反射有运行时开销 + 包体增大
- KSP 生成的代码错误能在编译期暴露(IDE 红线),反射要运行时才能发现问题
Q8. Kuikly 跟 Flutter 在架构上最大的区别是什么?
答:渲染层。
- Flutter:自带 Skia 渲染引擎,自己画所有像素,不依赖原生 UI 系统 → 跨端一致 + 包体大 + 系统组件需模拟
- Kuikly:复用各端原生 UI 系统(Android View / iOS UIView / ArkUI 等),只做"指令翻译" → 包体小 + 体验真原生 + 跨端有自然差异
哲学不同:Flutter 是"自己画一个 UI",Kuikly 是"用平台已有的 UI"。
Q9. 一次「按钮点击 → 屏幕更新」的完整链路是什么?
答:10 步:
- 用户点击原生按钮 View
- 原生层捕获 click 事件
- Bridge 上抛事件到 Kotlin 层
- Kotlin 层找到 viewId 对应的 click 监听器并执行
- 监听器修改 observable 字段
- observable setter 触发,把订阅 attr 块加入待重跑队列
- 下一帧调度时,批量执行待重跑队列
- 重跑产生新的 BuildTree → diff → 渲染指令
- 指令通过 Bridge 下发到原生层
- 原生层执行 setText / setBackground 等 → 屏幕刷新
Q10. 为什么说 Kuikly Android 端"几乎没有桥"?
答:Kuikly Android 端的 shared KMP 模块编译出的 .aar 就是 JVM 字节码,跟你写的普通 Kotlin Android 代码本质相同。
调用 Android View 时是普通的 JVM 方法调用,跟你在 MainActivity 里写 view.setText("...") 一模一样。没有序列化、没有消息队列、没有线程切换。
这跟 RN(JS ↔ Native 跨语言桥)有本质区别。这也是 Kuikly Android 端能做到「真·原生性能」的根本原因。
下一站 → 第 8 章 · 平台通信 Module / 调原生 API / expect-actual →
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
kotlin
/**
* 第 7 章配套代码 · expect / actual 跨端 API 范式
*
* 演示如何用 KMP 的 expect/actual 机制,写一个跨端的"获取平台信息" API。
*
* 这是 KMP 工程组织代码的"标准范式",第 8 章会进一步演化成 Module 概念。
*/
// ═══════════════════════════════════════════════════════════════════
// commonMain:公共声明(业务代码只调这一份)
// 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/platform/PlatformInfo.kt
// ═══════════════════════════════════════════════════════════════════
package com.example.kuikly.platform
/**
* 当前平台名称,如 "Android 14" / "iOS 17.0" / "HarmonyOS 4.0"
*/
expect fun currentPlatform(): String
/**
* 屏幕物理像素宽 × 高
*/
expect fun screenSize(): Pair<Int, Int>
/**
* 设备唯一 ID(演示用,真实场景注意隐私合规)
*/
expect fun deviceId(): String
// ═══════════════════════════════════════════════════════════════════
// androidMain:Android 平台实现
// 文件位置:shared/src/androidMain/kotlin/com/example/kuikly/platform/PlatformInfo.kt
// ═══════════════════════════════════════════════════════════════════
/*
package com.example.kuikly.platform
import android.os.Build
import android.provider.Settings
import com.example.kuikly.app.KuiklyApplication
actual fun currentPlatform(): String =
"Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})"
actual fun screenSize(): Pair<Int, Int> {
val ctx = KuiklyApplication.context
val dm = ctx.resources.displayMetrics
return dm.widthPixels to dm.heightPixels
}
actual fun deviceId(): String {
val ctx = KuiklyApplication.context
return Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
}
*/
// ═══════════════════════════════════════════════════════════════════
// iosMain:iOS 平台实现(Kotlin/Native)
// 文件位置:shared/src/iosMain/kotlin/com/example/kuikly/platform/PlatformInfo.kt
// ═══════════════════════════════════════════════════════════════════
/*
package com.example.kuikly.platform
import platform.UIKit.UIDevice
import platform.UIKit.UIScreen
actual fun currentPlatform(): String {
val dev = UIDevice.currentDevice
return "iOS ${dev.systemVersion}"
}
actual fun screenSize(): Pair<Int, Int> {
val bounds = UIScreen.mainScreen.bounds
return bounds.useContents { size.width.toInt() to size.height.toInt() }
}
actual fun deviceId(): String =
UIDevice.currentDevice.identifierForVendor?.UUIDString ?: "unknown"
*/
// ═══════════════════════════════════════════════════════════════════
// jsMain:Web / 小程序实现
// 文件位置:shared/src/jsMain/kotlin/com/example/kuikly/platform/PlatformInfo.kt
// ═══════════════════════════════════════════════════════════════════
/*
package com.example.kuikly.platform
import kotlinx.browser.window
actual fun currentPlatform(): String =
"Web ${window.navigator.userAgent}"
actual fun screenSize(): Pair<Int, Int> =
window.innerWidth to window.innerHeight
actual fun deviceId(): String =
window.localStorage.getItem("device_id")
?: ("web_" + (Math.random() * 1e10).toLong()).also {
window.localStorage.setItem("device_id", it)
}
*/
/* ─────────────────────────────────────────────────────────
* 业务代码(commonMain)使用方式:
*
* class HomePage : Pager() {
* override fun body() = {
* Text { attr { text("当前平台: ${currentPlatform()}") } }
* Text { attr { text("屏幕: ${screenSize()}") } }
* Text { attr { text("设备: ${deviceId()}") } }
* }
* }
*
* Android 上看到:"Android 14 (SDK 34)"
* iOS 上看到:"iOS 17.0"
* Web 上看到:"Web Mozilla/5.0..."
*
* ★ 一份 commonMain 业务代码,3 端结果都对 —— 这就是 expect/actual 的威力
* ─────────────────────────────────────────────────────── */kotlin
/**
* 第 7 章配套代码 · 渲染指令模型(教学版伪代码)
*
* 这是 Kuikly 真实指令的简化版,帮助理解"DSL → 指令 → 原生"的流程。
* 真实代码在 com.tencent.kuikly.core.render.* 包下。
*
* 不要直接编译进生产工程,仅用于学习参考。
*/
package com.example.kuikly.learn.render
// ═══════════════════════════════════════════════════════════════════
// 1. 指令模型(6 大核心指令)
// ═══════════════════════════════════════════════════════════════════
sealed class RenderCommand {
/** 创建一个原生 View */
data class CreateView(
val viewId: Int,
val viewType: String, // "View" / "Text" / "Image" / ...
val parentId: Int? = null,
) : RenderCommand()
/** 设置 View 的某个属性 */
data class SetProp(
val viewId: Int,
val key: String, // "text" / "backgroundColor" / "width" / ...
val value: Any?,
) : RenderCommand()
/** 注册事件监听 */
data class AddEventListener(
val viewId: Int,
val event: String, // "click" / "scroll" / ...
) : RenderCommand()
/** 移除事件监听 */
data class RemoveEventListener(
val viewId: Int,
val event: String,
) : RenderCommand()
/** 把 child 挂到 parent 的指定位置 */
data class AddSubview(
val parentId: Int,
val childId: Int,
val index: Int = -1, // -1 = 末尾
) : RenderCommand()
/** 从 parent 移除 child */
data class RemoveSubview(
val parentId: Int,
val childId: Int,
) : RenderCommand()
}
// ═══════════════════════════════════════════════════════════════════
// 2. 跨端 Bridge 接口(每个平台实现一份)
// ═══════════════════════════════════════════════════════════════════
interface RenderBridge {
/** 下行:Kotlin 层 → 原生层下发指令 */
fun execute(commands: List<RenderCommand>)
/** 上行:原生层 → Kotlin 层抛事件回调 */
fun onEvent(viewId: Int, event: String, payload: Map<String, Any?>)
}
// ═══════════════════════════════════════════════════════════════════
// 3. Android 端 Bridge 实现(伪代码示意)
// ═══════════════════════════════════════════════════════════════════
/*
class AndroidRenderBridge(private val rootViewGroup: ViewGroup) : RenderBridge {
private val viewMap = mutableMapOf<Int, View>()
override fun execute(commands: List<RenderCommand>) {
commands.forEach { cmd -> executeOne(cmd) }
}
private fun executeOne(cmd: RenderCommand) = when (cmd) {
is RenderCommand.CreateView -> {
val view = when (cmd.viewType) {
"View" -> FrameLayout(rootViewGroup.context)
"Text" -> TextView(rootViewGroup.context)
"Image" -> ImageView(rootViewGroup.context)
else -> error("Unknown viewType: ${cmd.viewType}")
}
viewMap[cmd.viewId] = view
}
is RenderCommand.SetProp -> {
val view = viewMap[cmd.viewId] ?: return
when (cmd.key) {
"text" -> (view as? TextView)?.text = cmd.value as String
"backgroundColor" -> view.setBackgroundColor(cmd.value as Int)
"width" -> view.layoutParams = view.layoutParams.apply { width = cmd.value as Int }
"height" -> view.layoutParams = view.layoutParams.apply { height = cmd.value as Int }
// ... 几十种属性
}
}
is RenderCommand.AddEventListener -> {
val view = viewMap[cmd.viewId] ?: return
when (cmd.event) {
"click" -> view.setOnClickListener {
onEvent(cmd.viewId, "click", mapOf("x" to it.x, "y" to it.y))
}
}
}
is RenderCommand.AddSubview -> {
val parent = viewMap[cmd.parentId] as? ViewGroup ?: return
val child = viewMap[cmd.childId] ?: return
if (cmd.index >= 0) parent.addView(child, cmd.index) else parent.addView(child)
}
// ... 其他指令
else -> { }
}
override fun onEvent(viewId: Int, event: String, payload: Map<String, Any?>) {
// 通过 JNI 上抛到 Kotlin 层的事件分发器
EventDispatcher.dispatch(viewId, event, payload)
}
}
*/
// ═══════════════════════════════════════════════════════════════════
// 4. 业务侧使用示例(教学伪代码)
// ═══════════════════════════════════════════════════════════════════
/*
// 真实的 Kuikly DSL:
View {
attr {
backgroundColor(Color.WHITE)
size(100f, 50f)
}
Text { attr { text("Hello") } }
}
// 编译期 + 运行期最终生成的指令序列:
[
CreateView(viewId=1, viewType="View"),
SetProp(viewId=1, key="backgroundColor", value=0xFFFFFFFF),
SetProp(viewId=1, key="width", value=100),
SetProp(viewId=1, key="height", value=50),
CreateView(viewId=2, viewType="Text", parentId=1),
SetProp(viewId=2, key="text", value="Hello"),
AddSubview(parentId=1, childId=2),
]
// 这串指令通过 Bridge 下发到 Android 原生层,最终:
// FrameLayout (root, 100x50, 白底)
// └── TextView "Hello"
*/