
上一篇文章讨论了 KuiklyUI 的响应式系统,主要看 observable 怎么拦截读写、ReactiveObserver 怎么收集依赖、状态变化后怎么只重新执行相关的 attr 块。那篇文章提到了布局属性的变化会触发后续的布局重算,但没展开。这篇文章接着往下,深入布局和测量这两件事。
先通过一张流程图看布局与测量具体是怎么工作的。

上方是输入,一棵 view 树和每个节点上的样式约束。中间是跑在 Kotlin 共享层的布局引擎,收到样式变更后标脏、检查是否需要重算,然后进入两件事:布局算法根据样式约束算坐标和尺寸,测量确定文字这类内容自身的宽高。图中橙色虚线标出了测量需要走 Bridge 到 Native 再回填的往返路径。这一去一回是同步调用,Kotlin 侧停住等 Native 返回尺寸才继续。底部是最终结果通过 SET_RENDER_VIEW_FRAME 下发到四个平台。
这张图把布局引擎的输入、处理、输出和下游都标清楚了。在深入引擎之前,先把布局和测量这两个概念拆开看清楚。
布局做的事很直接:拿到一棵 view 树,给每个节点一个 (x, y, width, height)。
各个平台都内置了布局能力。Android 的 ViewGroup 走 measure() 再 layout(),iOS 的 AutoLayout 用约束方程求解,Web 由 CSS 引擎从样式规则推导盒模型。方案虽然不同,但做的事情是一样的:把开发者写的宽高、间距、对齐方式翻译成屏幕上实际的像素位置。
KuiklyUI 的情况要麻烦一些。它的 view 树在 Kotlin 共享层,桥的另一端是五个不同的原生平台。如果布局也分别交给各平台做,同一个 margin(10f) 在 Android 的 weight 计算和 iOS 的 hugging priority 下可能算出差几像素的结果。所以 KuiklyUI 选择在 Kotlin 侧自己算,这套算法参考了 Facebook Yoga 的 Flexbox 模型,后面会细讲。布局的结果是每个节点的 frame,也就是 x、y、width、height 四个值。
测量是布局的前置步骤,但只对 Text 这类节点有意义。
大部分节点的尺寸是写死的:width(100f) 就是 100,不需要额外信息。但 Text 不一样。同样一段字,不同字体、不同字号、不同平台渲染引擎,算出来的宽度可能差几十像素。Kotlin 共享层不持有字体文件和排版上下文,所以 Text 不能像普通 View 那样从样式直接拿尺寸。
KuiklyUI 的做法是给 Text 注册一个测量回调,算法跑到这个节点时停住,通过 Bridge 同步调到 Native 侧,用各平台自己的排版引擎算真实尺寸,再传回来。这一去一回是同步调用,Kotlin 侧停住等结果,也是整个布局流程里唯一的同步跨语言调用点。
简单说:布局做的事是把样式约束翻译成坐标尺寸,测量做的事是给 Text 这类节点通过 Bridge 拿到真实宽高。两件事共享同一套脏标记和重算触发机制,布局负责算坐标,测量负责填 Text 的尺寸。
布局 | 测量 | |
|---|---|---|
做什么 | 样式约束 → (x, y, w, h) | 拿到文字的真实宽高 |
适用节点 | 所有 View / Text | 只有 Text 等含文字内容的节点 |
在哪个侧 | Kotlin 共享层 | 回调在 Kotlin,实际工作在 Native |
是否跨 Bridge | 否(全程 Kotlin 内存) | 是(同步调用,等 Native 返回) |
下面先看 KuiklyUI 为什么要把这两件事都放在共享层做。
每个平台的原生引擎都能做布局计算,Android 有 ViewGroup 体系,iOS 有 AutoLayout,Web 有 CSS 引擎。KuiklyUI 选择在 Kotlin 共享层自己做,三个原因。
第一,跨端一致性。同一个布局组合交给五个平台的原生引擎算,边界条件下会有像素级差异——AutoLayout 的 hugging priority、Android 的 weight 计算、CSS 的 margin collapsing,各自在不同的边界条件上偏差。把算法统一到 Kotlin 侧,同一套代码在五个平台上算出完全相同的坐标。
第二,布局算法本身不产生 Bridge 通信。Text 测量的同步调用(上一节已提到)是唯一的例外,除此以外,坐标和尺寸的计算全程在 Kotlin 内存中完成,只有最终结果才通过 SET_RENDER_VIEW_FRAME 下发。如果交给原生引擎,Native 侧的中间状态对 Kotlin 透明,Core 侧没法在算法中间做收敛判断和重试控制。
第三,不依赖原生 View 实例。整个布局工作都在共享层内存中进行,可以脱离原生 UI 系统独立测试。
Kotlin 侧需要一个布局引擎,接下来就是选算法。从头设计一套布局模型,不仅要处理 flex 弹性分配、折行、绝对定位、RTL 这些常规场景,还要覆盖大量 CSS Flexbox 规范里由无数业务验证过的边界条件。这个工作量不是一个团队能短期扛住的。
KuiklyUI 的选择是参考 Facebook Yoga 背后的 Flexbox 算法思路。Yoga 已经在 React Native 里被大规模验证过,模型成熟,边界条件也比较完整。KuiklyUI 在 Kotlin 侧实现了自己的 LayoutImpl,算法主流程和 Yoga / css-layout 高度相似,FlexNode、FlexStyle、FlexLayout 三层数据结构也分别对应节点、样式输入和布局输出。
用一段业务代码来看布局引擎实际接收什么样的输入:
View {
attr {
width(200f) // 固定容器宽度
flexDirection(FlexDirection.ROW) // 横向排列
}
Text {
attr {
text("Hello KuiklyUI") // 文字内容——宽度依赖测量
fontSize(16f)
flex(1f) // 参与剩余空间分配
}
}
View {
attr {
width(50f) // 写死宽高,不依赖测量
height(50f)
backgroundColor(Color.RED)
}
}
}业务代码里的 attr {} 调用链不直接对接布局算法。width(200f) 和 flex(1f) 最终落到 FlexNode 内部的 FlexStyle 上:
class FlexNode {
val flexStyle = FlexStyle() // 输入:attr {} 写入的属性
val flexLayout = FlexLayout() // 输出:算法算出的位置和尺寸
var measureFunction: MeasureFunction? = null // Text 才有
var isDirty = true
}
class FlexStyle {
var flexDirection = FlexDirection.COLUMN
var flex = 0f // 0 = 不参与 flex 分配
val dimensions = FloatArray(2) // [WIDTH, HEIGHT]
val margin = StyleSpace()
val padding = StyleSpace()
// ...
}从这个例子能看出布局引擎处理的两类节点。右侧 width(50f) 和 height(50f) 都写死的 View,尺寸由样式约束直接决定,布局算法不需要额外信息就能算。左侧 Text 不同。flex(1f) 表示它要占据剩余空间,但剩余空间有多大取决于容器宽度减去右侧 View 的 50px 之后还剩多少;除此之外,文字本身还有自己的最小宽度,这个宽度取决于字体、字号和平台渲染引擎,Kotlin 共享层不持有这些信息。Text 节点必须在创建时注册一个 measureFunction,布局算法运行到 Text 节点时,把样式宽度、父级约束或已分配的宽度作为测量约束,再调用 Native 侧拿到真实文字尺寸。这就是下一节要展开的测量链路。
移植不是照搬。Yoga 追求 CSS Flexbox 的完整实现,KuiklyUI 只保留了移动端高频的那部分。flex 只做 expand、不做 shrink,baseline 对齐和 gap 这些在 Column 纵向排列为主的场景里出场率极低,都没实现。
布局算法在做 flex 分配和坐标定位之前,需要先知道每个节点的尺寸。大部分节点的尺寸由样式约束直接决定,width(100f) 写死了就是 100。但 Text 不行:同样一段文字,不同字体、不同字号、不同平台渲染引擎,算出来的宽度不一样。这件事只能在 Native 侧做,Kotlin 共享层不持有字体和渲染上下文。
这意味着布局算法在碰到 Text 节点时,必须先拿到 Native 返回的真实文字尺寸,才能继续往下算坐标。布局依赖测量。

KuiklyUI 的做法是让 Text 类 View 在创建 FlexNode 时注册一个 measureFunction。LayoutImpl 在进入算法循环之前,先检查当前节点有没有注册测量函数,有的话调用它,拿到尺寸后再决定是否继续递归子节点。
链路图里标得很清楚:Bridge 协议统一,四个平台的 Shadow 各自独立实现测量。值得留意的是,如果宽高都已被样式指定,测量函数直接跳过,不触发 Bridge。
回头看整个方案:在 Kotlin 共享层移植 Yoga 做布局算法,测量则通过 Bridge 同步调用各平台原生能力。这个组合带来了几个明确的设计取舍。
收益方面,五端布局一致,算法统一,同一套样式约束算出同样的坐标。不依赖原生 View 实例,FlexNode 是纯内存结构,布局可以在脱离 UI 系统的环境下独立运行和测试。测量借助各平台原生能力,Android 的 Paint.measureText、iOS 的 NSAttributedString,每个平台用自己最准的工具,Kotlin 侧只拿结果。
代价也很明确。测量的同步 Bridge 调用是性能瓶颈,未命中缓存的 Text 需要等 Native 返回,长列表里积少成多。跨语言往返还带来了收敛问题:父容器宽度未定时传估算值,Native 回填真实尺寸后可能触发重算,最多迭代 3 轮,超了下一帧重试。Yoga 在同一进程内回调,没这个问题。
简化方面,参考 Yoga 但有意收缩了属性覆盖,flex 只做 expand、不做 shrink,不实现 baseline、gap、order。移动端以 Column 纵向排列为主,这些低频特性的出场率加起来极低,砍掉比实现带来的复杂度更划算。
这套方案的底线判断是:跨端一致性和测量准确性是刚需,同步 Bridge 的性能成本和收敛复杂度是可接受的代价。业务上真正成为瓶颈的通常不是单个 Text 的测量调用,而是频繁的布局重算本身。那部分由脏标记和缓存机制控制。
KuiklyUI 的布局方案由三块组成:Kotlin 共享层的 Yoga 移植做算法,各平台原生的 Shadow 对象做测量,Bridge 做两者之间的通信。算法参考了成熟的 CSS Flexbox 实现,测量借用了各平台最准的工具,通信走统一的 Bridge 协议。
这套方案的核心取舍是:用同步 Bridge 调用的性能成本和收敛复杂度,换跨端布局一致性和测量准确性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。