首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KuiklyUI 运行时闭环:从点击到屏幕更新

KuiklyUI 运行时闭环:从点击到屏幕更新

原创
作者头像
骑猪耍太极
发布2026-07-16 10:50:07
发布2026-07-16 10:50:07
30
举报

前面十三篇文章按模块拆开讲了 KuiklyUI 的主要机制:Pager、响应式、布局、指令、Bridge、Render、协程、Compose 接入、工具链、Module 和动画。模块拆开之后,每个机制都能看清楚,但读者心里还会留下一个问题:这些机制在一次真实交互里怎么串起来。

这一篇把视角拉回到一段最普通的业务代码。页面上有一个按钮,点击后 count++,Text 从 Count: 0 变成 Count: 1。这个动作看起来很小,底层却穿过了 Render、Bridge、Pager、事件系统、响应式系统、属性同步、布局测量和平台重绘。


一、从业务代码开始

先看一段简化后的页面代码:

代码语言:kotlin
复制
class HomePager : Pager() {
    var count by observable(0)

    override fun body(): ViewBuilder = {
        Text {
            attr {
                text("Count: ${ctx.count}")
            }
        }
        Button {
            attr {
                title("Add")
            }
            event {
                click {
                    ctx.count++
                }
            }
        }
    }
}

页面创建阶段,body() 在 Kotlin 侧构建内存 view 树。Text 的 attr 块读取了 ctx.count,响应式系统记录了依赖。Button 的 event 块注册了 click 事件,Core 会把这件事下发到 Render,让平台原生 View 监听点击。

页面首帧完成后,Native 侧已经有真实按钮和文本。接下来点击发生,链路开始反向流动。

把这段代码放进运行时里,大致会形成下面这条闭环:事件先从 Render 回到 Kotlin Core,状态变化后再把更新结果同步回 Render。后面的章节就按这张图的顺序展开。

一次点击背后的 KuiklyUI 运行时闭环
一次点击背后的 KuiklyUI 运行时闭环

二、事件先在 Render 层发生

点击最先发生在平台原生 UI 系统里。Android 侧会先经过 touch listener 和手势识别,再识别成 click;iOS 可以接 target-action,鸿蒙接 ArkUI 事件,Web 接 DOM event。不同平台入口不一样,但都会先落在 Render 层。

这些平台事件不会直接执行业务 Kotlin 代码。Render 在创建 Button 时已经收到过一条事件注册指令:

代码语言:bash
复制
SET_VIEW_PROP
arg0 = pagerId
arg1 = buttonTag
arg2 = "click"
arg3 = 1
arg4 = isEvent = 1
arg5 = sync

这里的 isEvent 表示这不是普通属性,而是事件订阅。Core 并没有把 Kotlin 闭包直接传给 Render,业务闭包仍然留在 Kotlin 侧的事件表里。Render 看到 isEvent = 1 后,会在平台侧包装出一个事件 callback,并把它注册到原生事件系统上。

点击发生时,Render 取到 pagerIdviewTageventName 和事件参数,通过上一篇 Bridge 文章里的 FIRE_VIEW_EVENT 往 Kotlin 侧回传,最终进入 BridgeManager.callKotlinMethod

到这里,事件已经从平台 UI 系统穿过 Bridge 回到了 Core。


三、Pager 找回对应的 View

BridgeManager 收到 FIRE_VIEW_EVENT 后,先根据 pagerId 找到对应的 Pager。Pager 内部维护了 view 索引,能够用 buttonTag 找回当初创建的 Button View。

代码语言:bash
复制
pagerId → Pager
buttonTag → Button View
eventName → click handler

随后事件系统触发业务代码里的闭包:

代码语言:kotlin
复制
click {
    ctx.count++
}

这一步看起来只是一次普通 Kotlin 函数调用,但它连接了两侧:事件在 Native 侧发生,业务逻辑在 Kotlin 侧执行。Bridge 和 Pager 的职责就是把这两侧按 pagerIdviewTag 对齐。


四、状态写入触发响应式系统

ctx.count++ 会被 Kotlin 委托属性拦截。observable 的写入最终进入 setValue,它会比较新旧值,如果值发生变化,就通知 ReactiveObserver

响应式系统手里有一张依赖表。Text 的 attr 块在页面创建阶段读过 count,所以这张表里已经记录了:count 变化时,需要重新执行 Text 的那段 attr 闭包。于是 setValue 通知 ReactiveObserverReactiveObserver 从依赖表里找到那段 attr 块并重新执行,text 被重新计算为 Count: 1

这里不会重新执行整个 body(),也不会重建整棵 view 树。KuiklyUI 的更新粒度收在 attr 块级别。对于一次文本更新,最终只需要重新计算 Text 的 text 属性。

这也是响应式系统和 Bridge 成本之间的配合:变化范围越小,下发到 Native 的指令越少。


五、属性变化重新走 Bridge 同步

Text 的 attr 重新执行后,text("Count: 1") 会进入属性系统。属性系统先比较旧值和新值。如果旧值是 Count: 0,新值是 Count: 1,这次变化就需要同步到 Render。

如果变化的是普通 View 属性,比如 opacitybackgroundColortransform,路径会比较直接:Attr.setProp 更新本地属性缓存后,经由 RenderView.setProp 发出 SET_VIEW_PROP

Text 稍微特殊一点。textfontSizetextColor 这类属性会先进入 TextShadow,因为文本需要参与测量。也就是说,text("Count: 1") 不只是一个普通属性更新,它还可能让 Text 的测量结果变化。源码里这条路径会把属性写到 shadow,并把对应的 FlexNode 标脏。

这次方向和点击回传相反:从 Kotlin 到 Native。简化后可以看成:

代码语言:bash
复制
Text.attr 重新执行
  ↓
Attr.setProp("text", "Count: 1")
  ↓
TextView.didSetProp
  ↓
TextShadow.setProp + FlexNode 标脏
  ↓
Bridge 同步 shadow / 布局 / 渲染属性

所以一次点击里 Bridge 至少会穿两次:第一次是事件上行,第二次是状态变化之后的属性或 shadow 同步下行。


六、Render 把变化应用到原生 View

Render 收到下行指令后,会根据 textTag 找到对应的原生组件。普通属性会走组件的 setProp,Text 这类需要测量的组件还会结合 shadow 数据一起处理。不同平台实现不同,但目标一致:把 Kotlin 侧算出的文本、样式和布局结果同步到真实 UI 对象上。

Android 侧最终会落到 Render 文本组件和它的 shadow,iOS 侧会落到对应的文本控件和测量对象,Web 侧会修改 DOM 或内部节点数据,小程序则会更新 view tree 数据并在合适时机 setData。

这个过程不需要 Core 知道平台细节。Core 只关心 tag、属性名、shadow 数据和 frame,Render 负责把这些统一信息翻译成平台 API。


七、布局是否参与由属性决定

一次点击不一定触发布局。比如 Text 设置了固定宽高,只是内容从 Count: 0 变成 Count: 1,最终尺寸可能没有变化。这个时候平台 UI 系统完成文本重绘即可。

但如果 Text 的宽度依赖内容,比如没有固定宽度,文字变长后就可能影响测量结果。这个时候链路会继续进入布局系统:TextShadow 先根据约束计算尺寸,必要时触发 CALCULATE_RENDER_VIEW_SIZE,FlexLayout 据此重新计算 frame,最后通过 SET_RENDER_VIEW_FRAME 下发。

这就是前面布局文章讲过的同步测量和 frame 下发。一次状态变化是否进入布局系统,不由响应式系统决定,而由变化的属性、shadow 测量结果和 FlexNode 脏标记共同决定。


八、列表和动画是同一条主链的变体

如果点击里不是 count++,而是 items.add(newItem),链路前半段一样:Render 捕获点击,Bridge 回传,Pager 找到 View,执行 click handler。差别从响应式更新开始。

ObservableList 会记录本轮集合操作,vfor 或 vforLazy 收到变化后,不是重新执行某个普通 attr,而是增量修改 view 树:创建新 item、插入到对应位置、必要时移除旧 item。随后下发的就不止 SET_VIEW_PROP,还会有 CREATE_RENDER_VIEWINSERT_SUB_RENDER_VIEWREMOVE_RENDER_VIEW 和布局 frame 更新。

如果点击里启动动画,前半段也一样。差别在属性下发前后会包一段 animation 属性:先告诉 Native 接下来的属性变化要走某个动画,再下发目标值,最后清掉动画上下文。动画本身由 Native 执行,Kotlin 不逐帧插值。

所以点击、列表、动画不是三套独立系统。它们共享同一条主链,只是在响应式之后走向不同的更新策略。


九、这条链路说明了什么

把一次点击串起来后,可以看到 KuiklyUI 的几个设计重心。

业务代码始终在 Kotlin 共享层执行。事件从 Native 回传到 Kotlin 后,状态变化和更新决策都在 Core 内完成,Render 不参与业务逻辑判断。

Bridge 只传必要信息。事件上行走 viewTag + eventName,属性下行走 tag + key + value,文本这类需要测量的内容再补一条 shadow 通道。它不传整棵 view 树,也不让 Render 知道业务状态。

Render 只负责平台落地。它知道怎么把属性、shadow 和 frame 变成平台文本、DOM 节点或小程序数据,但不知道 count 是什么、业务为什么要更新。

布局、指令和动画也不是三套独立系统。它们接在同一条更新主链上,只是被不同属性和不同数据结构变化所触发。


总结

一次 ctx.count++ 背后,KuiklyUI 走过了完整的运行时链路:Render 捕获原生事件,通过 Bridge 回到 Core;Pager 根据 tag 找到 View 并执行业务事件;observable 写入触发响应式系统;相关 attr 块重新执行;变化后的属性、shadow 或 frame 再通过 Bridge 同步给 Render;Render 最后把统一信息应用到平台原生 View 上。

这条链路把前面的文章串了起来。Pager 负责实例和 view 树,响应式系统负责找到更新范围,Bridge 负责跨运行时通信,Render 负责平台落地,布局和动画则根据属性变化按需介入。

回头看整个系列,KuiklyUI 的核心取舍其实很清楚:Kotlin 共享层负责业务状态、更新决策和 view 树组织;Bridge 保持固定、克制的跨运行时协议;Render 负责平台事件、原生组件和最终绘制。一次点击之所以能串起这么多模块,是因为这些模块本来就围绕同一条运行时主链设计。

理解这条主链之后,再看列表更新、Module 调用、Compose 接入和动画,就不再是散开的机制,而是同一套运行时在不同场景下的分支。到这里,主系列也完成了从分层拆解到端到端闭环的收束。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、从业务代码开始
  • 二、事件先在 Render 层发生
  • 三、Pager 找回对应的 View
  • 四、状态写入触发响应式系统
  • 五、属性变化重新走 Bridge 同步
  • 六、Render 把变化应用到原生 View
  • 七、布局是否参与由属性决定
  • 八、列表和动画是同一条主链的变体
  • 九、这条链路说明了什么
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档