
前面几篇文章都在讲运行时:Core 怎么建树,Bridge 怎么通信,Render 怎么落地,协程怎么处理页面异步任务。运行时机制弄清楚之后,还剩一个实际问题:业务工程怎么把 KuiklyUI 接进来。
跨端框架不只是写一套 Runtime。它还要面对 Kotlin 版本、Gradle 插件、KSP 代码生成、Maven 发布、多模块工程、动态化打包这些工程问题。这里先把几个名字放平:Gradle 负责构建,Maven 发布负责把框架产物发成依赖,KSP 是 Kotlin 的编译期代码生成工具。对业务来说,框架能不能跑起来,很多时候就卡在这些工具链细节上。
这一篇作为主题文章的收束,讨论 KuiklyUI 工具链里几个关键设计:多 Kotlin 版本支持、@Page 到 KuiklyCoreEntry 的 KSP 生成、多模块 Entry 合并,以及动态化产物。

打开 KuiklyUI 根目录,会看到多套 build.*.gradle.kts 和 settings.*.gradle.kts。它们对应不同 Kotlin 版本,从较早的 1.3、1.4,一直到 2.0、2.1,还有鸿蒙特化版本。
这在普通开源库里不常见。多数库会声明一个最低 Kotlin 版本,然后要求业务自己升级。但 KuiklyUI 面向的是生产业务,而生产业务升级 Kotlin 往往不是改一行版本号那么简单。它会牵动 Android 构建插件、KSP、三方库、CI 缓存和内部插件。
KuiklyUI 的选择是为不同 Kotlin 版本分别发布制品。业务用 Kotlin 2.0.21,就依赖 2.0.0-2.0.21;业务还停在 1.9.22,就依赖 2.0.0-1.9.22。
implementation "com.tencent.kuikly-open:core:2.0.0-2.0.21"
implementation "com.tencent.kuikly-open:core-render-android:2.0.0-2.0.21"
ksp "com.tencent.kuikly-open:core-ksp:2.0.0-2.0.21"这个版本号把两个维度编码在一起:KuiklyUI 自身版本和 Kotlin 编译器版本。业务可以直接看出当前制品对应哪个 Kotlin 版本。
代价也直接。仓库里要维护多套 build 文件,CI 要覆盖多个 Kotlin 版本,KSP 版本也要跟着映射。工具链复杂度上升,换来的是业务接入门槛下降。
KuiklyUI 页面通常会写一个 @Page 注解:
@Page("HomePage")
class HomePager : Pager() {
override fun body(): ViewBuilder = {
// ...
}
}运行时 Native 侧发起 CREATE_INSTANCE 时,手里拿到的是页面名或路由字符串。Core 侧要把这个字符串变成一个具体 Pager 实例。这里可以有两种做法:运行时反射查找,或者编译期生成路由表。
KuiklyUI 选择 KSP。可以把 KSP 理解成 Kotlin 编译期的扫描器和代码生成器:它在编译时找到带注解的类,再生成一份普通 Kotlin 代码,最后和业务代码一起参与编译。

KSP 在编译期扫描所有带 @Page 的类,收集页面名和类名,然后用 kotlinpoet 生成平台对应的 KuiklyCoreEntry。生成代码里会注册类似这样的映射:
BridgeManager.registerPageRouter("HomePage") { HomePager() }
BridgeManager.registerPageRouter("DetailPage") { DetailPager() }运行时创建页面时,就不需要反射扫描类,也不需要保留运行时注解。@Page 的 Retention 是 SOURCE,编译后注解可以消失,信息已经被转成了生成代码。
这个设计的收益明确:启动路径更短,不依赖反射,不容易被 R8/ProGuard 影响,Kotlin 类型检查也能覆盖生成代码。代价是业务工程需要配置 KSP。不过对一个以 Kotlin 为核心的跨端框架来说,这个代价通常可以通过 Gradle 插件和模板工程消化掉。
KuiklyCoreEntry 不是只有一份。Android、iOS、鸿蒙、Web 的运行时边界不同,生成代码也要适配不同平台。
Android 版 Entry 实现 JVM 侧的接口,Render 可以反射加载它,并通过 delegate 建立 Kotlin 到 Render 的回调通道。iOS 版 Entry 需要暴露给 ObjC,所以会带上 ObjC 命名相关处理,并在边界上包异常上报。鸿蒙版 Entry 要处理 Kotlin/Native 与 C++ 的互操作。Web 版则面向 Kotlin/JS 和全局函数桥。
KSP 处理器会根据当前编译目标选择不同生成器。当前源码里能明确看到 Android、iOS、鸿蒙三类入口生成器;其他目标会走默认入口生成路径。Web 侧最终运行在 Kotlin/JS 和全局函数桥上,但这里不单独展开入口生成细节。
这也解释了为什么工具链和运行时是绑在一起的。Bridge 协议虽然统一,但进入 BridgeManager 之前,每个平台需要不同的入口适配。KSP 正好把这些差异提前放到编译期解决。
单模块项目里,KSP 扫描所有 @Page 然后生成一个 Entry 就够了。但真实业务通常不只有一个模块。常见结构是:
app/
├── feature_home/
├── feature_detail/
└── feature_settings/每个子模块里都可能有自己的 @Page。如果每个模块都生成一个同名 KuiklyCoreEntry,主工程编译时会重复定义。KuiklyUI 的方案是给子模块加一个 moduleId,也就是模块标识。子模块生成带模块标识的注册对象,主模块生成最终 Entry,并聚合所有子模块注册函数。
简化后是这样的:
feature_home 生成 KuiklyCoreEntryAndroid_home.registerPages()
feature_detail 生成 KuiklyCoreEntryAndroid_detail.registerPages()
app 主模块生成 KuiklyCoreEntryAndroid:
init {
KuiklyCoreEntryAndroid_home.registerPages()
KuiklyCoreEntryAndroid_detail.registerPages()
}子模块只负责声明自己有哪些页面,主模块负责把这些页面汇总到运行时入口里。这样新增页面仍然靠 @Page,新增模块只需要补 KSP 参数,不需要手写注册代码。
这个设计对业务接入很关键。跨端页面往往按业务域拆模块,如果入口生成不支持多模块,框架会在工程组织上卡住。
KSP 不是一个和 Kotlin 完全解耦的工具。它工作在 Kotlin 编译期,会直接依赖 Kotlin 编译器 API,所以不同 Kotlin 版本需要匹配不同 KSP 版本。
KuiklyUI 的构建变量里维护了 Kotlin 到 KSP 的映射。例如 Kotlin 1.9.22 对应一个 KSP 版本,Kotlin 2.0.21 对应另一个版本。业务如果 Kotlin 和 KSP 版本错配,编译期就会失败。
这也是多 Kotlin 版本支持复杂的原因之一。不是换一套 kotlinVersion 就结束了,KSP、Android 构建插件和其他编译期依赖都可能跟着变。KuiklyUI 把这些变量集中到 buildSrc 里,再用多套 Gradle 文件固定下来,减少业务自己拼版本的机会。
从业务视角看,这种设计有点重;从框架维护视角看,它是在把版本兼容问题前置到制品发布阶段,而不是让每个业务接入时各自踩坑。
KuiklyUI 工具链里还有一个更贴近业务价值的方向:动态化。它解决的不是代码怎么编译,而是业务页面能不能独立发布。活动页、小流量实验、线上 bug 修复,如果每次都等 App 整体发版,跨端框架的迭代效率会被应用市场流程卡住。
动态化的核心,是把业务页面编译成可以被宿主拉取和加载的产物。不同平台的产物形态不同:Android JVM 模式下 .class 跟 App 一起发,不属于动态化主路径;动态化时可以走 Native 或 Web 模式。iOS 常规是 K/N framework,受平台政策限制,动态化更多依赖 Web 模式。鸿蒙可以加载业务动态库,Web 则天然是 JS bundle。
这条链路大致是这样:
编辑 HomePager.kt
↓
按页面或模块打包业务代码
↓
生成 JS bundle / Native 动态产物
↓
上传到 CDN 或动态化平台
↓
App 运行时拉取指定版本
↓
Render 加载业务代码
↓
CREATE_INSTANCE 创建 Pager这里 KSP 也参与了动态化。@Page 注解里除了页面名,还有 supportInLocal 和 moduleId 这类信息:前者表示页面是否支持内置打包,后者表示页面属于哪个业务模块。KSP 处理器还会读取一些构建参数,用来决定这次应该打哪些页面。比如只打某一个页面,或者只打某几个业务模块。这样动态包不一定要包含所有页面。对大型业务来说,活动页、频道页、实验页可以拆成独立产物,而不是每次都带上整套页面。
工具链里围绕 packLocalJsBundle、packLocalAarBundle 这类开关和打包任务,把业务代码打成可下发产物。实际工程里也会有按页面打包的 debug / release 任务,最终目的都是一样的:让宿主在运行时拿到一份新的业务代码,并把它交给 Render 加载。
动态化带来的收益很直接:修 bug、新增活动页、做灰度实验,不一定要等 App Store 或应用市场发版。它也让跨端页面更适合承接高频变化的业务,比如营销活动、临时运营位、A/B 实验。
代价也同样明确。动态包要控制体积和加载耗时;下发前要做版本匹配和安全校验;线上要有灰度、回滚、缓存和失败兜底;调试链路也会比静态依赖复杂。KuiklyUI 把动态化纳入工具链,说明它关注的不只是 demo 能跑,而是真实业务怎么持续发布。
这也是为什么 KuiklyUI 的工具链不能只停留在 Maven 依赖。它还要支持业务代码被打包成可下发产物,并能在不同运行时里被加载。
多 Kotlin 版本支持还是强制业务升级,KuiklyUI 选前者。代价是维护多套构建配置和 CI 矩阵,但门槛降低带来的业务接入量值得这个投入。
KSP 生成入口还是运行时反射,选了 KSP。启动更快,不依赖反射,不容易被混淆工具影响。代价是业务需要接入 KSP 插件。
多模块自动聚合还是手动注册,选了自动聚合。业务不用手写样板代码,代价是 KSP 参数和生成逻辑更复杂。
只提供静态依赖还是支持动态化产物,选了把动态化纳入工具链。业务能独立发版,代价是构建、加载、调试和安全都更重。
这些选择共同指向一个目标:让 KuiklyUI 不只是能在 demo 里跑,而是能进入真实业务工程。真实业务工程的问题往往不在 Runtime 本身,而在版本、模块、发布和接入边界上。
KuiklyUI 的工具链解决的是运行时之外的工程问题。多 Kotlin 版本制品让不同业务能按自己的编译器版本接入;KSP 把 @Page 注解转换成平台 Entry,避免运行时反射;多模块 Entry 合并让大型业务可以按模块拆分页面;动态化打包让业务代码具备独立发布能力。
这些设计都不轻,但它们对应的是跨端框架落地时绕不开的问题。Core、Bridge、Render 决定框架能不能运行,工具链决定它能不能被业务长期使用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。