
上一篇文章从整体架构入手,把 KuiklyUI 拆成业务层、Compose DSL、Core、Bridge、Render 几个部分。我们已经知道 KuiklyUI 分成了多个层级,现在讨论的重点放在另一个问题上:为什么要这样分层。
分层是软件工程里最基础的架构模式之一。从操作系统的内核态和用户态,到网络协议栈,再到 Web 前后端分离,分层解决的是同一个问题:把系统拆成职责清晰的若干部分,每部分只和相邻层交互,层内细节对外透明。跨端 UI 框架天然适合分层,共享逻辑和平台渲染逻辑天然就是两层。
KuiklyUI 把分层落到三个具体问题上:哪些代码共享、哪些代码各端独立、两边怎么通信。Core、Bridge、Render 三层分别对应这三个问题。
跨端框架需要同时处理两件互相矛盾的事:最大化代码共享,和保留足够的平台原生能力。
两个需求天然冲突:共享意味着统一,原生意味着差异,共享多了压不住平台特性,差异留多了稀释共享价值。
跨端框架的架构设计,本质上是在回答共享和差异的边界划在哪里。
业务逻辑,比如点击后 count++,显然该共享。操作原生 View,比如把 count 显示在屏幕上,显然不该共享,各平台有自己的 UI 系统。中间还有通信:共享层的逻辑结果怎么传给原生层。通信本身也是需要设计的对象:什么格式、什么方向、什么粒度,哪部分通信代码通用,哪部分通信代码独立。
这三个问题各有自己的约束条件,各有自己的设计选择。下面从 Core、Bridge、Render 三层的角度分别展开。
Core 面对的核心问题是:怎么让尽可能多的逻辑跑在共享层,同时不依赖任何平台的 UI 类型。
限制很清楚。共享层可以用 Kotlin 标准库,可以用协程,甚至可以做布局计算。布局算法是纯数学,只负责算出坐标和尺寸,不关心最终画出来的是 Android 的 Canvas 还是 iOS 的 Core Graphics。但共享层绝对不能碰 android.view.View 或 UIView,一旦依赖了具体平台的 UI 类,跨平台的前提就不存在了。
KuiklyUI 用 KMP 的 expect / actual 机制在共享和平台之间划界。这里有一个判断标准值得拆开看:什么算平台 UI 类型,什么不算。
如果把所有和平台沾边的逻辑都塞进各平台的 xxxMain 目录,那 commonMain 会很干瘪,xxxMain 会很臃肿,共享的价值不大。KuiklyUI 的标准更松:commonMain 适合容纳任何不直接引用平台 UI 类的逻辑。
这个标准下,FlexLayout 的布局算法在 commonMain,因为它只是数学计算,不碰 UI 对象。响应式系统在 commonMain,observable 的依赖收集和变更通知是纯 Kotlin 逻辑。指令系统 vfor / vif / vbind 在 commonMain,它们操作的是 KuiklyUI 自己的 view 树。BridgeManager 通信入口也在 commonMain,它只负责生成指令和分发回调,不碰具体的平台通信机制。
各平台目录 androidMain、iosMain、ohosArm64Main、jsMain 主要处理必须接触平台 API 的事:NativeBridge 对接通信机制、JSON 引擎适配序列化能力、异常处理对齐平台习惯。
DateTime 是个典型例子。commonMain 用 expect object DateTime 声明了三个函数——时间戳、高精度单调时间、线程 CPU 时间,各端的 API 完全不同,但通过 expect/actual 对上同一套签名:

DateTime 只是其中之一。布局引擎、响应式系统、BridgeManager 也都是同样的模式——要么是 commonMain 里的纯逻辑,要么走 expect/actual 划界。
这个选择的效果是共享层有了独立的体量和演进能力。布局引擎的算法优化了,五个平台同步受益。新增一个平台需要补的只是 xxxMain 里的 actual 实现,commonMain 不用改。
但宽松的划界标准也带来一个具体的代价。expect/actual 意味着任何一种平台能力被新增到共享层,五个平台都要补一份 actual 实现,少一个就会让共享层的调用编译失败。新增一个平台时,工作量集中在补 actual 而不是写业务代码。平台差异的 bug 也通常以 expect/actual 不匹配的形式暴露,定位时要逐平台对照检查。这种依赖是精确的边界控制,但也意味着没有模糊地带可走:每个平台差异都要明确处理,不能在共享层里打补丁。
Bridge 面对的问题比 Core 更具体:Core 在 Kotlin 侧,Render 在各平台的原生侧,两边不在同一个运行时里,不能直接函数调用。Bridge 这一层要解决的就是怎么在这种跨语言边界上保持高效和稳定。
所有跨端框架都要处理这个问题,各自的方案不同。
同样是跨运行时通信,不同框架会把优先级放在不同位置。有的方案更强调调试直观,消息里直接带字符串 method 和结构化参数;有的方案更强调类型化协议,在性能和可读性之间取平衡。KuiklyUI 这里选的是固定 methodId 加参数槽位,主路径上没有 JSON 解析,也没有字符串 key 匹配。这个选择不是说其他方案不好,而是它更看重协议稳定性和主路径性能。
KuiklyUI 的 Bridge 一共 23 个 methodId,分两个方向。Native 到 Kotlin 只有 6 个,全部是事件和生命周期通知;Kotlin 到 Native 有 17 个,全是操作指令。数量不对称是因为业务逻辑都在 Kotlin 侧。
每个调用的签名固定为 6 个参数槽位,第一个永远是 instanceId。Render 侧的实现就是一个 when(methodId) 分发——没有 JSON 解析,没有字符串 key 匹配。代价是调试不直观,看到 callNative(4, ...) 需要查表才知道是 SET_VIEW_PROP;复杂参数也还是得走 JSON,只是被放到了少数复杂调用里,不在主路径上。

一次交互 Bridge 穿了两次:事件上行走 FIRE_VIEW_EVENT,属性下行走 SET_VIEW_PROP。所有跨层交换都走这同一条通道,Core 和 Render 之间没有第二条路径。
Render 面对的矛盾:五个平台各有自己的 UI 系统和开发生态,怎么让它们各自高效、又不至于行为发散。
KuiklyUI 的选择是各端 Render 独立工程——Android 用 Kotlin/Java 对接 Android View,iOS 用 ObjC/C++ 对接 UIKit,鸿蒙用 ets + C++ 对接 ArkUI,Web 用 Kotlin/JS 对接 DOM。同一个 Text 组件在五个平台上对应五种完全不同的原生实现:

Render 和 Core 之间是 compileOnly 依赖——编译期有类型,运行时不带实现,Core 内部重构不影响 Render。好处是发版解耦:修一个平台的 Render bug 不拖累其他四个平台发版。
代价是新增一个基础组件要五端各自写一份原生渲染逻辑。但这部分工作量不大——布局计算、事件分发、属性管理都在 Core 的 commonMain 里做完了,Render 只负责把给定的属性值应用到原生 View 上。另一个代价是跨平台 bug 定位:一个功能在 iOS 上异常,需要排查 Render 层、Core 层还是 Bridge 协议理解不一致,走一遍五端对照。
三层各自选了方案之后,它们的协作规则收敛到一点:层和层之间只能通过 Bridge 通信。没有隐藏的调用路径,没有绕过协议的捷径。
这个约束的效果是,Core 和 Render 的内部重构对彼此尽量透明。Core 把 Pager 内部数据结构换了,Render 不需要感知,它只通过 Bridge 的 instanceId 标识页面。Render 内部换一种平台实现方式,Core 也不关心,它只通过 Bridge 下发属性值。只要 23 个 methodId 语义不变,两边可以各自独立演进。
从分层设计看 KuiklyUI,每个选择背后都有明确的约束条件,也都有它放弃的东西。这些选择不是孤立成立的——Core 的共享程度决定 Bridge 需要传什么,Bridge 的协议设计决定 Render 需要实现什么,三层互相约束、互相支撑。后续文章会沿着这些选择在具体机制里怎么落地继续展开。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 [email protected] 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 [email protected] 删除。