首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一个 KuiklyUI 页面是怎么创建出来的

一个 KuiklyUI 页面是怎么创建出来的

原创
作者头像
骑猪耍太极
发布2026-06-13 16:10:57
发布2026-06-13 16:10:57
990
举报

前三篇文章讲了 KuiklyUI 的分层结构、Core/Bridge/Render 各自的职责和它们为什么这样分工。这一篇换一个角度:从一个页面被宿主 App 打开开始,到用户在屏幕上看到第一帧,中间到底发生了什么。

先说一个会在后面反复出现的判断:KuiklyUI 的页面创建不是一次性渲染,而是分成了两个阶段。先在 Kotlin 侧构建一棵完整的内存 view 树,再在 createBody() 阶段把这棵树逐节点翻译成 Native 侧的原生 View 树。


一、页面在 KuiklyUI 里的角色

在讲创建链路之前,需要先看清楚 Pager 在 KuiklyUI 里的角色。

宿主 App 打开一个跨端页面后,Core 侧需要有一个对象承接这个页面实例:它要保存页面 ID,管理生命周期,持有 view 树,也要在 Native 事件回传时负责路由。

在 KuiklyUI 里,这个对象叫 Pager。第一篇提过它是页面抽象、拥有完整生命周期,但那只是它身份的一面。

Pager 在 KuiklyUI 里有一个更加底层的角色:它不只是页面对象,同时是整棵 view 树的根节点。

Pager 的双面角色
Pager 的双面角色

你写的 View { }Text { } 这些组件,最终都作为子节点挂在 Pager 下面。Pager 不是站在树外面管理这棵树,它就是树本身。这个身份决定了后续很多事情:生命周期回调挂在这里、ReactiveObserver 以它为隔离单元、viewMap 的注册以它为入口、Native 事件回传也通过它的 pagerId 路由到正确的实例。


二、从路由字符串到 Pager 实例

第一节讲了 Pager 是页面抽象同时也是 view 树的根节点。Native 通过 Bridge 把创建信号发给 Kotlin 侧时,附带的是一个页面地址字符串。Kotlin 侧手里需要拿到一个具体的 Pager 子类实例——中间这段"字符串到实例"的翻译,KuiklyUI 分两步完成:编译期生成路由表,运行时按表创建。

第一步发生在编译期。KuiklyUI 用 KSP(Kotlin Symbol Processing,Kotlin 的编译期代码生成工具)扫描所有带 @Page 注解的类,为每个目标平台生成一份入口文件。可以把它简单理解成构建期生成路由表,只不过 KSP 运行在 Kotlin 编译器内部,可以直接访问类型信息。

KSP 从扫描到路由表就绪的完整流程:

KSP 编译期路由注册 → 运行时 Pager 创建
KSP 编译期路由注册 → 运行时 Pager 创建

关键点:到这一步为止,Kotlin 侧还没有任何 Pager 实例,只有一张路由表。KSP 的作用是把页面地址和 Pager 子类之间的映射关系,在编译期确定下来。页面路由到 Pager 子类这一步,运行时不再靠反射查找。

第二步是运行时。Native 侧发起 CREATE_INSTANCE 调用后,BridgeManager 根据 methodId 路由到 PagerManager.createPager(),后者从路由表找到对应的工厂函数、new 出 Pager 实例,然后进入 onCreatePager() 启动生命周期。具体每一步的触发时机和执行顺序,第三节的生命周期图会更直观地展开。


三、第一阶段:构建内存 view 树

第二节末尾说到 Pager 实例创建后进入 onCreatePager(),启动生命周期。完整的时序见下图:

CREATE_INSTANCE 到首帧的完整生命周期
CREATE_INSTANCE 到首帧的完整生命周期

这张图里有一条明确的分界线:didInit()body() 执行完之后,图上标了 ⚠ 到此都只是 Kotlin 内存操作,Bridge 指令还没发出。以此为界,前面的操作全部在内存中完成;后面的 createBody() 才开始向 Native 发指令。页面创建分成内存阶段和渲染阶段两段,这一节看前半段。

onCreatePager() 先跑三步准备:willInit() 留给子类覆盖,initModule() 注册内置 Module(惰性创建,只存工厂函数不实例化),didMoveToParentView() 把 Pager 自己注册进 view 索引。跑完这三步,Pager 环境就绪,但页面上还什么都没有。

接下来执行业务代码写的 DSL 块,在内存中把整棵 view 树搭起来。这个过程是深度优先的:每遇到一个 View {} 块,创建一个 view 实例,把 attr {} 里的属性存入内存,把 event {} 里的回调存入内存,如果是容器型 View 就继续递归处理子节点。attr {} 块里如果读了 observable 字段,依赖关系也在这一步建立(具体机制下一篇讲)。

所有这些操作只在内存里完成,不发任何 Bridge 指令。跑完之后,内存中有一棵完整的 view 树,每个节点的属性、事件、父子关系全部就绪。但 Native 侧完全不知道这棵树的存在。下一节 createBody() 才是把它翻译成渲染指令的起点。


四、第二阶段:翻译成渲染指令

第三节结尾说,body() 执行完后内存中有一棵完整的 view 树,Native 侧还不知道它的存在。createBody() 的任务就是把这棵树逐节点翻译成 Native 能执行的指令。它内部分四步:

createBody 内部四步与 Bridge 指令时机
createBody 内部四步与 Bridge 指令时机

第一步 createFlexNode():递归遍历 view 树,为每个节点创建一个 FlexNode(布局计算用的数据结构)。这一步还是纯计算,不发 Bridge 指令。

第二步 createRenderView():从这里开始发 Bridge 指令。递归遍历 view 树,每个节点依次做三件事:发 CREATE_RENDER_VIEW 让 Native 创建对应的原生 View,发 SET_VIEW_PROP 把之前积累的属性逐条下发,发 INSERT_SUB_RENDER_VIEW 挂到父节点下面。先设属性再挂节点的顺序是刻意的,如果反过来,子 View 挂上去时属性还不齐,Native 侧后续的布局和渲染时机更难控制。

第三步 insertToRootView():Pager 自身的 RenderView 用约定的 ROOT_VIEW_TAG = -1 挂到 Native 根容器上。其他所有 View 都通过 INSERT_SUB_RENDER_VIEW 挂在父 View 下面,只有 Pager 直接挂在容器顶层,这是它作为根节点的特殊待遇。

第四步 layoutIfNeed():FlexNode 跑布局算法,算出每个节点的坐标和尺寸,结果通过 SET_RENDER_VIEW_FRAME 下发给 Native。布局最多迭代 3 轮:第一轮基于约束算出初始 frame,但 Text 这类组件需要异步从 Native 拿真实测量尺寸(文字宽度受字体渲染影响),回填后相关节点重算;两轮通常稳定,第三轮留余量。超过 3 轮还没收敛,投递到下一帧异步重试。


五、首帧绘制与回调

layoutIfNeed() 完成后,Native 侧有了完整的 View 层级和布局坐标。接下来各平台走自己的绘制管线:Android 的 measure/layout/draw、iOS 的 UIKit 渲染、Web 的浏览器合成器。这部分 KuiklyUI 不介入。

当 Native 侧完成第一帧绘制,通过 Bridge 回报一个 UPDATE_INSTANCE 事件(eventName 为 "pageFirstFramePaint"),Core 侧收到后触发 onFirstFramePaint() 回调。

Core 侧 view 树创建完毕和用户真正看到画面之间有一段不可忽略的时间差。跨端框架需要由 Native 回报首帧状态,不能假定 Core 侧工作做完用户就已经看到了。


总结

Pager 不只是页面对象,它同时也是整棵 view 树的根节点。业务代码的组件层层挂在它下面,生命周期、响应式上下文、事件回传都以它为中心组织。

页面创建分两个阶段。第一阶段在内存中构建完整的 view 树,不产生任何 Bridge 通信;第二阶段 createBody() 才把内存树逐节点翻译成 Native 渲染指令。这个分离让 Core 层可以先完整准备好一棵树,再批量推给 Render 层。代价是时序约束变多:attr 依赖在 body() 阶段建立、viewMap 注册紧跟 didMoveToParentView、RenderView 创建只能在 createBody() 里做,这些步骤不能随意调换顺序。

首帧的绘制和确认跨了两个阶段。Native 走自己的绘制管线,完成后通过 Bridge 回调通知 Core 侧。跨语言边界两侧的状态不是同步推进的。

下一篇进入响应式系统:既然 attr {} 在 body() 阶段建立了依赖关系,那么后续状态变化时,KuiklyUI 怎么精确地只重新执行相关 attr 块并下发 Bridge 指令。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、页面在 KuiklyUI 里的角色
  • 二、从路由字符串到 Pager 实例
  • 三、第一阶段:构建内存 view 树
  • 四、第二阶段:翻译成渲染指令
  • 五、首帧绘制与回调
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档