最近被一个 HarmonyOS 6 + ArkTS 的改版需求缠住了:首屏金刚区要从 4 列改成 6 列,另一个运营位要做固定两行的横向滚动入口。听起来就是 Grid 固定行列的小事,columnsTemplate、rowsTemplate 两个字符串一填,最多再管下间距,应该半小时能收工。真正上手以后才发现,网格出来了,但多出来的 item 去哪了?行高为什么不受控?明明设了固定列,数据一多整个容器又为什么开始纵向滚?这个周末我把 Grid 固定行列的规则彻底跑了一遍,把踩过的坑、验证过的结论和各种可直接复制的写法整理成一篇偏实操的记录,新手能照着抄,写过几版的老手也能确认一些容易忽略的边界行为。
1. Grid不是普通布局容器:先看清它“滚动表格”的本质
很多开发者一开始把 ArkTS 里的 Grid 当成 CSS Grid 或者传统 UI 里的宫格控件来用,所以一上来就问:为什么我设置了 4 列,第 5 个 GridItem 没有换行排到第二行,它跑哪去了?要回答这个问题,必须先接受一个事实:在 ArkUI 里,Grid 的身份不是普通布局容器,而是“带网格能力的滚动容器”,它和 List 属于同一个家族。List 是单维滚动列表,Grid 是二维滚动网格,两者底层都围绕滚动、复用、缓存这些机制来运转。这一点决定了你琢磨固定行列的时候,不能只想着格子怎么排,还要想滚动的方向、容器的高度、数据超出后如何处理。
1.1 只设置 columnsTemplate:行数交给数据自己长
先看最常见场景:固定列数,行数随数据增加,显示不下时容器滚动。代码只需要设置 columnsTemplate,不需要碰 rowsTemplate。
typescript复制Grid() {
ForEach(this.demoList, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height(80)
.textAlign(TextAlign.Center)
.backgroundColor('#F1F3F5')
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.width('100%')
.height('100%')
这个写法下,1fr 1fr 1fr 1fr 把横向可用宽度切成了 4 份,每个 GridItem 默认占用一个单元格。数据有 8 个,就渲染成两行;数据有 100 个,就继续往下排,容器高度不够时整体竖向滚动。也就是说,“固定列数”只是固定了模板方向,另一个维度是由 GridItem 数量和容器高度共同决定的动态维度。这也是需求改动中最常出现的行为:我要 4 列,只设置 columnsTemplate 就够了,不需要在 rowsTemplate 里写死到底生成多少行。
1.2 只设置 rowsTemplate:列数从右侧悄悄延伸
反过来,只设置 rowsTemplate 就是固定行数,列由数据在水平方向上扩展。这个场景适合做高度固定、横向滑动的两行入口,很多应用的“宫格第二屏”就是这种结构。
typescript复制Grid() {
ForEach(this.horizontalList, (item: string) => {
GridItem() {
Text(item)
.width(100)
.height(64)
.textAlign(TextAlign.Center)
.backgroundColor('#E8F3FF')
.borderRadius(8)
}
}, (item: string) => item)
}
.rowsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('100%')
.height(140)
设置 rowsTemplate: '1fr 1fr' 表示整个 Grid 的内容区域严格按两行来组织,所以 GridItem 会先填满第一列的两个位置,然后继续在右侧生成新列。数据多了以后,内容会超出屏幕宽度,此时 Grid 在水平方向变成可滚动状态。注意这里的高度我给了一个具体值:外层如果没有明确高度,Grid 拿不到确定的行高分配依据,布局表现就会变得奇怪。
1.3 两个模板都设置:二维单元格数量会被锁死
同时设置 columnsTemplate 和 rowsTemplate,这大概是需求文档里最字面意义上的“固定行列”。比如 2×2 宫格、3×3 宫格,模板就限定了 Grid 内部最多能显示 rows × columns 个单元格。
我在设备上验证时发现一个新手最容易懵的现象:如果同时设置了 rowsTemplate: '1fr 1fr' 和 columnsTemplate: '1fr 1fr',循环里却给 Grid 塞了 6 个 GridItem,最终渲染出来的往往不是正常的 3 行 2 列,也不是前 4 个保留后面隐藏,而是多出来的子项挤在同一个布局体系里导致行为不可预测,有的版本里后续子项会直接不显示,有的会把单元格的行列关系打乱。所以做这种固定棋盘式布局时,子项数量必须和模板行列数严格对应,不要指望 Grid 像 FlowLayout 那样自动换行并扩展隐式网格。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. columnsTemplate与rowsTemplate字段写法:fr单位、空格与固定尺寸怎么组合
固定行列的第二个大坑是模板字符串本身。columnsTemplate 和 rowsTemplate 接收的是一个字符模板,不是数组,不是对象,也不是逗号分隔的字符串,而是以空格分隔的一串轨道定义。我在代码评审里见过有人把 CSS 里的写法直接搬过来写 '1fr, 1fr, 1fr',结果运行时不报错但列数完全是乱的,调试了半天才发现是分隔符问题。
2.1 模板字符串的组成规则
模板里每一段代表一列(或者一行),常用的有几种:
1fr:按比例分配剩余空间。三个1fr就是三等分。- 固定数值 + 单位:比如
120vp、100px,表示该轨道不吃弹性,严格占固定宽度。 - 混合写法:比如
'100vp 1fr 1fr',表示第一列固定 100vp,剩下两列平分剩余区域。
多个轨道之间必须用空格隔开。想要生成动态列数的模板,不要手工拼一长串字符串,用循环生成更稳:
typescript复制const columnCount = 5
const template = new Array(columnCount).fill('1fr').join(' ')
// 得到 '1fr 1fr 1fr 1fr 1fr'
这段逻辑看着简单,但确实解决了不少实际问题。运营后台返回的分类数量不是写死的,比如今天配置 4 个金刚区入口,明天改成 6 个,模板数量跟着数据源长度变,代码里只维护一个 columnCount 变量就可以。
2.2 fr 和固定宽度的选择原则
如果网格里的每个入口权重相同,比如金刚区图标、九宫格菜单,直接用 1fr 做均分最简单。但如果是要做类似表格的布局,比如商品列表里第一列是缩略图,第二列是名称价格,第三列是操作按钮,就不适合全用 fr。缩略图应该固定成 88vp,操作按钮也可以固定成 64vp,中间的信息列用 1fr 去吸收剩余宽度。使用固定轨道时需要注意父容器宽度不足情况,如果所有固定轨道加起来已经超过容器实际宽度,Grid 自身会陷入横向空间不足的状态,子项可能被裁剪,这种时候就要考虑减少固定列数量或者允许 Grid 横向滚动。
表格可以这么总结:
| 模板写法 | 含义 | 适用场景 |
|---|---|---|
'1fr 1fr 1fr' |
三等分 | 金刚区、菜单宫格 |
'120vp 1fr' |
第一列固定,第二列自适应 | 头像 + 信息列表 |
'1fr 2fr' |
非等分比例 | 重点突出某一列 |
'88vp 1fr 64vp' |
多列混合 | 类表格结构 |
行模板同样遵循这套规则,rowsTemplate: '56vp 1fr' 可以做出“顶部固定标题行 + 下方内容滚动”的效果,但真正要做到“标题行不跟着内容滚”,还需要处理滚动容器结构,这一块放到后面冻结行列的部分详细说。
2.3 和 CSS Grid 对照:语法像,用法完全不同
阮一峰那篇 CSS Grid 教程在社区流传很广,很多人对 grid-template-columns、gap 这套概念是有感觉的。ArkUI 设计模板字符串时显然是受了 CSS Grid 启发,fr 单位、轨道概念都和 CSS 很像。但两者的本质差别在于承载场景:CSS Grid 是文档流里的二维布局系统,它不会自己滚动,宽度不够就压缩,子项放不下会根据 grid-template-rows 生成隐式行;ArkUI 的 Grid 则是一个虚拟滚动容器,它默认认为内容可能超过一屏,滚动方向由未固定的那个维度决定。
理解这个差异,很多诡异现象就通了。同样的四列布局,CSS 里放 20 个子项,Grid 容器会自动撑高把全部内容展示出来;ArkTS 里放 20 个,超出容器高度后你必须滚动才能看到后面的内容。所以做固定行列前,先问自己:这个区域需要滚动吗?
- 不需要滚动、数据固定少量,比如个人中心顶部 2×2 的服务入口,直接用
Row+Column嵌套实现反而更简单,没必要上 Grid。 - 需要滚动、数据可能动态变化,才应该用 Grid,并且明确哪个方向固定、哪个方向滚动。
3. 四个能直接复制的固定行列场景代码
这一节我按真实需求把常见场景拆开,每个场景给一个最小可运行示例。这些代码都在 DevEco Studio 的工程里验证过基础行为,照着放到自己的页面里,替换数据源和控件就能用。
3.1 金刚区固定列数、行数自适应
首页金刚区是最经典的应用:每行展示 4 个图标入口,七八个入口排成两行左右,超过一屏继续向下滚动也可以。只需要设置 columnsTemplate: '1fr 1fr 1fr 1fr',rowsTemplate 不要设置,让 Grid 按数据量自动生成行。
typescript复制@Entry
@Component
struct EntryGridDemo {
private entries: Array<string> = []
aboutToAppear(): void {
for (let i = 1; i <= 9; i++) {
this.entries.push(`入口${i}`)
}
}
build() {
Column() {
Grid() {
ForEach(this.entries, (item: string) => {
GridItem() {
Column() {
Text('图')
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(10)
Text(item)
.fontSize(12)
.fontColor('#182431')
.margin({ top: 6 })
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height(88)
.backgroundColor('#F1F3F5')
.borderRadius(12)
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.width('100%')
.height('100%')
}
.padding(16)
.width('100%')
.height('100%')
}
}
这里的 Grid 撑满整个页面高度。如果数据只有 3 个,你会看到只有一行铺 3 个格子,右侧留空。实际产品里为了视觉统一,后端一般会补齐空位,数量不够时前端可以动态补 null 占位项,这个根据业务约定处理就好。
3.2 固定两行的横向滚动宫格
运营位经常需要横向滑动查看更多,比如“猜你喜欢”楼层里每屏固定两行,左右滑动加载。固定行数时用 rowsTemplate。
typescript复制@Entry
@Component
struct HorizontalGridDemo {
private items: Array<string> = []
aboutToAppear(): void {
for (let i = 1; i <= 12; i++) {
this.items.push(`卡片${i}`)
}
}
build() {
Column() {
Grid() {
ForEach(this.items, (item: string) => {
GridItem() {
Text(item)
.width(96)
.height(56)
.textAlign(TextAlign.Center)
.backgroundColor('#FFF3E5')
.borderRadius(8)
}
}, (item: string) => item)
}
.rowsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.width('100%')
.height(132)
}.padding({ left: 16, bottom: 16 })
.width('100%')
}
}
注意,Grid 的子项加宽时,虽然没有显式设置 Grid 的滚动方式,但一旦所有列的总宽度超过 Grid 可视宽度,横向滑动行为就会出现。如果出现的是整体 Grid 向上下扩展而不是横向滚动,优先检查 rowsTemplate 有没有写对,比如写成 rowsTemplate: '1fr 1fr' 后行数应该被固定为两行,数据不会继续向下生长。
3.3 2×2 固定宫格:子项数量和模板必须精确匹配
固定 2×2 的快捷操作区,一般是功能固定的卡片入口,比如支付、扫码、卡包、设置四宫格。行列模板都固定起来,代码很直接:
typescript复制@Entry
@Component
struct FixedSquareGridDemo {
private actions: Array<string> = ['支付', '扫码', '卡包', '设置']
build() {
Grid() {
ForEach(this.actions, (item: string) => {
GridItem() {
Column() {
Text('●')
.fontSize(26)
.fontColor('#0A59F7')
Text(item)
.fontSize(14)
.fontColor('#182431')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.width('100%')
.height(180)
}
}
这个例子里 actions 数组恰好 4 个,和模板的组合完全匹配。如果后端给的数据是 6 个,多出来的两个 GridItem 不一定按你期望的“隐藏”处理,所以建议在装配数据时做一次截断或过滤。我在实际开发中会先在数据源层保证数量不超过 rows × columns,并约定不足补空对象,这样布局层只需要关注模板本身。
3.4 首行首列固定效果:用嵌套 Grid 模拟冻结表格
需求里如果出现“左侧渠道固定、右侧数据横向滚动”这种冻结首列的效果,ArkUI 的 Grid 本身没有把某个轨道钉死在可视区的能力。最简单的做法是用 Row 拆成左右两个区域,左边一个普通 Grid 或 List 固定宽度,右侧再放一个 Grid 负责滚动。首行冻结也类似,表头用 Row 实现,下方内容区用 Grid 滚动。
实现“表头 + 滚动区”时最麻烦的是列宽对齐。左右两个 Grid 都使用相同的 columnsTemplate,比如 '100vp 1fr 1fr',右边滚动区的首列如果不想被滚走,需要在数据模型里为表头部分单独占位,否则只通过 GridItem 的 columnStart/columnEnd 做列合并是可以跨列的,但不能把滚动容器里的第一列变成绝对定位。所以实际业务里,冻结首列的工程量通常比想象中大,除非交互上允许整块横向滑动,否则我会建议产品经理换一种方案。
4. 固定行/列的滚动方向、复用缓存和容器高度问题
固定行列不是写完模板就结束,滚动方向和容器高度会直接影响最终效果。这部分是我调样式时花时间最多的地方,几个容易被忽略的细节集中说一下。
4.1 滚动方向不是由属性指定,而是由“未固定方向”决定
ArkUI 的 Grid 不像某些第三方网格库那样需要显式给 scrollDirection。只要设置 columnsTemplate,竖向就是内容扩展方向,超过高度触发纵向滚动;只要设置 rowsTemplate,列就开始横向扩展,超过宽度触发横向滚动。两个模板都设置,如果子项刚好铺满,滚动行为不会出现;如果子项超出了双向行列数,实际渲染本身就不规范,所以这种场景里要格外控制数据量。
理解这一点后,你就不需要刻意去记哪个属性控制横竖滚动,只需要想清楚:需求里哪一维度是定死的,把该维度模板写上;另一个维度想让数据怎么跑,它就会怎么滚。想把 4 列固定再横向滚动,这种组合本身是矛盾的,除非你修改 columnsTemplate 让容器宽度容纳不下,否则列的扩展方向取决于 rowsTemplate 是否设置,而不是你去猜测。
4.2 数据量超过一屏时:ForEach 和 LazyForEach 的选型
如果是固定列数、数据量只有二三十条,用 ForEach 完全没有问题。但如果是固定列数加滚动列表,数据可能达到几百上千,直接用 ForEach 一次性创建所有 GridItem 会导致首屏性能下降明显,滑动时也会感到掉帧。
Grid 的懒加载机制需要配合 LazyForEach 使用。LazyForEach 要求传入一个数据源,这个数据源实现 IDataSource 接口,包含 totalCount()、getData(index)、注册数据监听等方法。大体结构是:
typescript复制class GridDataSource implements IDataSource {
private dataList: Array<any> = []
private listeners: Array<DataChangeListener> = []
totalCount(): number {
return this.dataList.length
}
getData(index: number): any {
return this.dataList[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
}
addData(item: any): void {
this.dataList.push(item)
this.listeners.forEach(listener => {
listener.onDataAdd(this.dataList.length - 1)
})
}
}
LazyForEach 的接线本身值得单独写一篇,这里只需要记住结论:用 Grid 做固定行列加滚动时,数据长度不确定且可能变大,统一走 LazyForEach,不要图省事把 ForEach 直接用于大数据量。
4.3 容器高度给错,固定行列会整体变形
固定行列非常依赖 Grid 自身的测量约束。Grid 虽然可以像列表一样滚动,但它的高度框架不是由内容撑起来的,而是在布局阶段明确分配出来的。放在 Column、Row、Stack 等容器里时,如果父组件不限制高度,Grid 会尝试占满父组件剩余空间,或者出现高度为 0 的极端情况;在 Scroll 里再嵌一个 Grid,问题会更明显,因为外层滚动容器给内层 Grid 提供的往往是无限高度约束,Grid 就会按照所有子项全部展示去计算,此时行列模板和懒加载可能全部失效,表现成一次性渲染非常长的内容。
所以固定行列的 Grid 最好避免嵌套在纵向 Scroll 里。如果页面结构必须整体滚动,建议把 Grid 改为非滚动布局实现,比如把 Grid 里那一块换成 Row/Column 展开。Grid 和 Scroll 各自都带滚动体系,嵌套后手势归谁、缓存怎么算都容易乱。
5. 固定行列最容易翻车的5个瞬间
下面这些坑不是一次踩完的,是不同需求里陆续冒出来的。每一条都配了现场表现和解决思路,遇到相似问题能少走很多弯路。
5.1 columnsTemplate 没错,但 GridItem 默认每个只占一格
这是一个容易和“跨行跨列”混淆的问题。GridItem 默认占一个单元格,如果你想让某个子项跨两列显示,比如运营推荐位的大图卡,必须用 GridItem 的 columnStart、columnEnd、rowStart、rowEnd 参数去声明。这个行为和 CSS Grid 里的显式网格项定位是一样的。
typescript复制GridItem() {
Text('大图推荐位')
.height(120)
}
.columnStart(0)
.columnEnd(1)
记住参数是闭区间还是开区间要查当前 SDK 文档,不同版本的 ArkUI 对 rowEnd 的定义曾有变化,我用的时候会先随手造一个两列的 Grid 快速验证一下边界。
5.2 同时固定行列后,第 5 个子项到底去哪了
这就是文章开头提到的诡异现象。同时设置 rowsTemplate 和 columnsTemplate 之后,Grid 会尝试把子项放进一个固定行列的结构里。子项数量超出时不会出现“自动往下再多排一行”的情况,它超出了模板规划的区域,布局结果就和预期脱节。我之前排查过一个反馈,用户说某个卡片在折叠屏上偶发消失,最后发现就是数据多了一个,模板写死了 3×2,6 个格子塞了 7 个数据。问题不在 Grid,而在业务侧没有对数据数量做限制。
验证方法很简单:在 GridItem 里临时加个背景色,数一下实际渲染的格子数量就知道有没有超出。处理方法是数据源在上层过滤,或者根据模板数量动态截取列表。
5.3 给 Grid 包了一层 Scroll,滚动手势时灵时不灵
这个坑在固定行列的页面里非常常见。页面主体是长页面,中间某一块又想做 Grid,有人下意识就用 Scroll 包住整个页面,Grid 放里面。实测会发现纵向滑动时经常出现一卡一卡,有时 Grid 区域滑动没反应,有时会连续跳好几屏。
原因就是两个滚动体系抢手势。外层 Scroll 对整个页面滚动,Grid 内部也在监听滑动手势。要解决,首先要判断页面上除了 Grid 还有多少其他内容需要跟着滚动。如果只有 Grid 一个长内容,就把 Scroll 去掉,直接用 Grid 当页面滚动主体;如果页面确实有非 Grid 的头部、介绍等模块,考虑用 List 的 header 结构把头部内容当成列表项,或者把 Grid 放进 ListItem 中让 List 统一滚动,而不是 Scroll 套 Grid。
5.4 GridItem 里的文字被压缩,溢出内容把布局撑变形
固定行列模式下,GridItem 的尺寸是由模板瓜分出来的,子组件自适应能力有限。如果一个 Text 文字过长,默认可能会换行,把卡片高度撑高,但 GridItem 本身轨道高度已经被 rowsTemplate 限制,最终表现为文字溢出、截断,或者卡片内布局错位。
给 GridItem 内的文本统一做约束是常规操作:
typescript复制Text(this.longText)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
如果不需要换行,用 .maxLines(1),配合 TextOverflow.Ellipsis 显示省略号。这样即使数据源里出现超长标题,固定行列的视觉也不会崩。
5.5 数据刷新后 Grid 停在原滚动位置
固定行列的横向滚动列表,数据下拉刷新后,如果希望回到顶部或最左侧,需要让 Grid 感知滚动位置。ArkUI 里 Grid 支持通过滚动控制器控制位置。我习惯在数据刷新完成后主动把滚动位置归零,不做这一步,用户刷新后看到的是中间或末尾的内容,产品极有可能会提 bug。
注意这种方式要求滚动控制器和 Grid 建立关联,控制器在页面销毁时注意释放,避免造成不必要的引用。
6. 封装一个固定列数的响应式网格组件
固定行列的场景一旦写顺了,很容易发现页面之间大量逻辑是重复的:一个数据数组、一个列数、一个间距,外加每个卡片自己的 UI。把这些抽成一个受控组件,后续接入新页面会省很大精力。
一个最基础的封装思路是这样:
typescript复制@Component
export struct FixedColumnGrid {
@Prop columnNum: number = 4
@Prop columnGap: number = 12
@Prop rowGap: number = 12
@Prop dataList: string[] = []
@Builder
itemBuilder(item: string) {
Text(item)
.width('100%')
.height(80)
.textAlign(TextAlign.Center)
.backgroundColor('#EEF0F3')
.borderRadius(8)
}
build() {
Grid() {
ForEach(this.dataList, (item: string) => {
GridItem() {
this.itemBuilder(item)
}
}, (item: string) => item)
}
.columnsTemplate(new Array(this.columnNum).fill('1fr').join(' '))
.columnsGap(this.columnGap)
.rowsGap(this.rowGap)
.width('100%')
.height('100%')
}
}
调用时只需要传列数和数据:
typescript复制FixedColumnGrid({
columnNum: 4,
dataList: this.menus,
columnGap: 10,
rowGap: 10
})
这个封装里有个关键点:columnsTemplate 是用数据属性动态生成的,属性变化时 Grid 能感知并重新布局。如果把它放在 build 里每次 new 一个数组,也正常,但如果组件刷新很频繁,建议把模板字符串缓存到成员变量里,避免纯 UI 刷新时反复创建数组。
如果需要根据容器宽度自动改变列数,可以通过 onAreaChange 监听 Grid 区域宽度变化,再换算列数。实际换算时要考虑宽度单位,以及模板里的固定间距,不能简单粗暴地拿总宽除以卡片宽度,否则最后算出来的列数加所有间距会超出容器,出现横向滚动。稳妥做法是:
- 先让布局引擎拿到容器实际宽度;
- 用“容器宽度 + 列间距”作为总可分配宽度;
- 根据单列最小宽度估算最大列数;
- 用估算结果更新 columnNum。
这套逻辑在折叠屏和横屏场景下特别有用。HarmonyOS 的多窗口宽度变化比手机单屏频繁得多,如果每个页面都手写“监听宽度 + 重算模板”,工作量非常大。封装一次,后续页面上 Grid 的固定列只需要面对业务数据和单个卡片视图,布局适配规则统一从组件层控制,效率和稳定性都会好不少。
如果要做跨行跨列的运营卡片,比如某个入口要占两列,可以在数据项里增加 rowSpan、colSpan 字段,GridItem 创建时动态绑 columnStart/columnEnd,这样封装就从“固定行列”演变成了“可配置不规则网格”,单个运营位的大小、位置就能由后端配置驱动,前端模板代码基本不用变。
我个人落地的习惯是:能用 Row/Column 解决的小宫格绝不上 Grid,但一旦场景里存在横向或纵向滚动,就只让 Grid 这一个容器承担滚动职责,不要再嵌套别的滚动容器。模板字符串用统一方法生成,不散落在各处。开发过程中多留几个“多一个数据会不会崩”的测试用例,比运行时去调布局要高效得多。
