首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KuiklyUI Bridge 如何连接 Core 和 Render

KuiklyUI Bridge 如何连接 Core 和 Render

原创
作者头像
骑猪耍太极
发布2026-07-11 12:06:40
发布2026-07-11 12:06:40
100
举报

上一篇文章讲了指令系统。vfor 插入一行 item、vif 切换一段子树、vbind 重建一块 UI,最后都会回到同一个问题:Kotlin 共享层改完 view 树之后,怎么把变化同步到 Native。Bridge 正是这条同步路径的边界。

前面几篇文章反复提到 Bridge,但更多是把它当作背景:页面创建时要发 CREATE_RENDER_VIEW,响应式更新时要发 SET_VIEW_PROP,布局完成后要发 SET_RENDER_VIEW_FRAME。这一篇把 Bridge 单独拿出来,看它的协议形态、双向通信、回调系统和几个关键取舍。


一、Bridge 解决的是跨运行时边界

KuiklyUI 的 Core 跑在 Kotlin 共享层,Render 跑在各个平台自己的运行时里。Android 是 JVM,iOS 是 ObjC/UIKit,鸿蒙是 C++/ArkTS,Web 是 JS/DOM。共享层不能直接拿到 android.view.View,也不能直接调用 UIView 的方法。

所以 Core 和 Render 之间需要一条稳定通道。它既要能表达创建 View、设置属性、插入子节点、调用 Module 这些动作,又不能让每个平台各自发明一套格式。

KuiklyUI 的做法很克制:所有通信都收敛到一组固定 methodId 和 6 个参数槽位。

代码语言:kotlin
复制
callNative(methodId, arg0, arg1, arg2, arg3, arg4, arg5)
callKotlin(methodId, arg0, arg1, arg2, arg3, arg4, arg5)

methodId 表示要做什么,arg0 固定是 pagerId,后面几个参数按 methodId 的语义解释。这个设计看起来不如 JSON RPC 直观,但它对跨语言很友好:四个平台只要能传基本类型、字符串和空值,就能接上同一套协议。

Bridge 23 method 双向通信总览
Bridge 23 method 双向通信总览

这张图把 Bridge 的协议形态压缩到一张表里。它不是复杂的消息总线,而是一套很薄的 RPC 协议。薄带来的好处是路径短、稳定、容易在不同平台落地;代价是阅读时要查 methodId,参数语义也更多依赖框架内部约定。


二、两个方向的职责不对称

Bridge 有两个方向。

Native 到 Kotlin 方向只有 6 个 method,主要是生命周期和事件通知:创建页面、更新页面事件、销毁页面、触发回调、触发 View 事件、请求重新布局。这个方向的特点是 Native 主动告诉 Core:外部世界发生了什么。

Kotlin 到 Native 方向有 17 个 method,都是操作指令:创建 RenderView、删除 RenderView、插入子 View、设置属性、设置 frame、计算测量尺寸、调用 View 方法、调用 Module、创建 Shadow、设置定时器、同步刷帧、上报异常等。这个方向的特点是 Core 主动告诉 Render:现在需要做什么。

这种不对称很自然。业务逻辑都在 Kotlin 侧,状态变化、指令生成、布局计算也都在 Kotlin 侧,所以 Kotlin 往 Native 发出的指令种类更多。Native 侧更多是接收指令和回报事件。

把前面几篇文章串起来看:

代码语言:bash
复制
页面创建:CREATE_RENDER_VIEW / SET_VIEW_PROP / INSERT_SUB_RENDER_VIEW
响应式更新:SET_VIEW_PROP
布局完成:SET_RENDER_VIEW_FRAME
Text 测量:CALCULATE_RENDER_VIEW_SIZE
指令增删节点:CREATE_RENDER_VIEW / REMOVE_RENDER_VIEW / INSERT_SUB_RENDER_VIEW
事件回传:FIRE_VIEW_EVENT

这些看似分散的机制,最后都落在这 23 个 method 上。也就是说,Bridge 是 Core 和 Render 之间唯一的公共语言。


三、属性、布局和事件共用同一条通道

高频的下行调用是 SET_VIEW_PROP。它承担两个职责:设置普通属性,也注册事件。

SET_VIEW_PROP 属性与事件复用通道
SET_VIEW_PROP 属性与事件复用通道

普通属性很好理解。比如 Text 的内容变化后,Core 会下发类似这样的调用:

代码语言:bash
复制
methodId = SET_VIEW_PROP
arg0 = pagerId
arg1 = textViewTag
arg2 = "text"
arg3 = "count = 1"
arg4 = 0
arg5 = 0

Render 收到后找到对应 tag 的原生 TextView、UILabel 或 DOM 节点,把文本设置进去。

事件注册也走同一个 method,只是 isEvent 标记不同。业务写 event { click { ... } },Core 侧仍然下发 SET_VIEW_PROP,只是告诉 Render 这次设置的是事件。Render 收到后不会把它当普通属性,而是把它转成平台事件监听。点击事件发生时,Render 再通过 FIRE_VIEW_EVENT 回到 Kotlin。

这个复用是一个小但重要的选择。属性和事件本来可以分成两个 method,但它们的形态很接近:都绑定在某个 viewTag 上,都有一个 key,都要写入 Render 侧的 view 对象。合并后协议更少,Render 侧实现也更集中。

布局则走另一条更直接的路径。FlexLayout 在 Kotlin 侧算出 frame 后,通过 SET_RENDER_VIEW_FRAME 下发 (x, y, width, height)。这里不走 JSON,也不走复杂对象,直接传四个 Float。布局是高频路径,参数越简单越好。


四、Module 调用和回调系统

View 指令解决的是 UI 更新,Module 调用解决的是系统能力。网络、路由、存储、通知这些能力不在 Kotlin 共享层里实际执行,最终要回到平台侧。

KuiklyUI 没有给每个 Module 单独设计 Bridge method,而是统一走 CALL_MODULE_METHOD

代码语言:bash
复制
methodId = CALL_MODULE_METHOD
arg0 = pagerId
arg1 = moduleName
arg2 = methodName
arg3 = params
arg4 = callbackRef
arg5 = syncCall

moduleName + methodName 决定 Native 侧调哪个模块、哪个方法。复杂参数通常会序列化成 JSON 字符串。这个选择让 Bridge 协议保持稳定:新增一个业务 Module,不需要新增 methodId,只要在 Native 侧注册同名模块即可。

回调系统需要单独看。Kotlin lambda 不能直接跨语言传给 Native,KuiklyUI 用的是全局函数表加字符串引用。

Module callback 回调闭环
Module callback 回调闭环

流程可以简化成四步:

  1. Kotlin 侧把 callback 注册到 GlobalFunctions,拿到一个字符串 callbackRef
  2. CALL_MODULE_METHODcallbackRef 发给 Native。
  3. Native 异步任务完成后,通过 FIRE_CALLBACK 带着 callbackRef 和结果回到 Kotlin。
  4. Kotlin 侧按 pagerId + callbackRef 找回 lambda 执行,默认执行后销毁。

这里没有跨语言函数引用,只有字符串 ID。函数留在 Kotlin 内存里,Native 只拿到一个引用号。这个模型和很多 RPC 系统一样,朴素但可靠。

pagerId 分桶也很关键。页面销毁时,属于这个 Pager 的回调可以整桶清理,避免异步任务回来后还访问已经销毁的页面。


五、JSON 和原生值的边界

Bridge 的控制字段偏向原生值:Int、Float、String、Boolean、ByteArray。比如 frame 的四个数字直接传,viewTag 直接传 Int,methodId 直接传 Int。这些路径不需要解析 JSON,开销可控。

复杂 payload 不可避免要走 JSON。页面参数、事件数据、Module 参数、部分复杂样式,最后都会变成字符串。KuiklyUI 在 commonMain 里提供了一套轻量 JSON API,各平台再接自己的 JSON 能力。Web 端甚至有一份自实现的 JSON 引擎,用来保证同步解析能力。

二进制数据是另一条特殊路径。图片、Pb 这类数据如果先 base64 再塞进 JSON,会有明显体积和性能损耗。KuiklyUI 为 ByteArray 保留了原生传递通道,各平台再转成自己的二进制表示。这个细节说明 Bridge 的设计不是单纯追求协议统一,而是在高频和大数据路径上留了性能出口。


六、同步与异步的边界

大多数 Bridge 调用是异步的。Core 下发创建 View、设置属性、插入节点后,Render 可以排队到 UI 线程执行。这样不会让 Kotlin 侧长时间等待平台 UI 操作完成。

但有些调用需要同步返回。典型场景是 Text 测量:布局算法运行到 Text 节点时,需要立刻知道文字真实宽高,才能继续计算父子节点的位置。这个时候 Core 调 CALCULATE_RENDER_VIEW_SIZE,Native 侧用平台排版能力算出结果并同步返回。

Module 也支持同步调用,但只适合很轻的能力,比如内存缓存读取、base64 编解码这类不会阻塞的操作。网络请求、文件 IO 这种耗时能力仍然应该走异步回调。

同步调用让流程简单,但它会把两个运行时绑在同一个调用栈里。用得太多,会把 Bridge 变成性能瓶颈。KuiklyUI 的策略是只把少数需要同步返回的能力留在同步路径上,大部分 UI 操作仍然异步排队。


七、这套协议的设计取舍

回头看 Bridge 的设计,可以归成几个明确取舍。

固定 methodId 不走字符串命令,分发快、协议稳定、跨语言实现也简单。代价是不直观,读日志时看到 methodId 需要查表才知道含义。

6 个参数槽位不走可变参数列表,各平台实现同一套函数签名就行。槽位用满时会出现复用。鸿蒙的 syncCall 字段就复用了 bit 来携带 keepAlive 信息,算是固定位宽协议下的小适配。

Module 统一走一个 method,自定义 Module 不需要改 BridgeManager。代价是 Native 侧要拿 moduleName + methodName 做二次分发。

控制字段走原生值,复杂 payload 走 JSON,高频场景开销可控。两套参数形态并存也让 Render 实现需要处理更多类型判断。

这些取舍没有追求协议表达上的完整与直观,而是把稳定性放在更靠前的位置。


总结

Bridge 是 KuiklyUI 里连接 Core 和 Render 的唯一通道。它把所有跨运行时通信压缩成 23 个 methodId 和 6 个参数槽位:Native 到 Kotlin 负责生命周期、事件和回调,Kotlin 到 Native 负责创建、属性、布局、Module 和系统能力。

这套协议的核心特点是瘦、固定、可跨语言。它不追求可读性最强,而是优先保证多平台落地简单、主路径性能可控、协议长期稳定。响应式系统、指令系统、布局系统、Module 系统,最后都通过这条通道和 Render 发生关系。

下一篇会继续沿着 Bridge 往下,看各个平台的 Render 层如何接住这些 method,并把它们转换为 Android View、UIKit View、ArkUI 节点和 DOM 节点。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Bridge 解决的是跨运行时边界
  • 二、两个方向的职责不对称
  • 三、属性、布局和事件共用同一条通道
  • 四、Module 调用和回调系统
  • 五、JSON 和原生值的边界
  • 六、同步与异步的边界
  • 七、这套协议的设计取舍
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档