首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KuiklyUI 布局与测量的设计取舍

KuiklyUI 布局与测量的设计取舍

原创
作者头像
骑猪耍太极
发布2026-07-07 13:15:30
发布2026-07-07 13:15:30
240
举报

上一篇文章讨论了 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 高度相似,FlexNodeFlexStyleFlexLayout 三层数据结构也分别对应节点、样式输入和布局输出。

用一段业务代码来看布局引擎实际接收什么样的输入:

代码语言:kotlin
复制
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 上:

代码语言:kotlin
复制
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 返回的真实文字尺寸,才能继续往下算坐标。布局依赖测量。

测量流程:Text 通过 Bridge 获取 Native 文字尺寸
测量流程:Text 通过 Bridge 获取 Native 文字尺寸

KuiklyUI 的做法是让 Text 类 View 在创建 FlexNode 时注册一个 measureFunctionLayoutImpl 在进入算法循环之前,先检查当前节点有没有注册测量函数,有的话调用它,拿到尺寸后再决定是否继续递归子节点。

链路图里标得很清楚: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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、布局和测量到底是什么
  • 二、在共享层代码计算布局
  • 三、参考成熟布局算法
  • 四、布局算法依赖测量
  • 五、布局与测量方案的设计考量
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档