LCUI 2.0.0 开发日志

2019年12月18日  ·  6347 字  ·  阅读

开发计划:

  • 重写布局系统,完善对 Flex 布局支持。
  • 改进部件的更新流程,只需遍历一次部件树就能完成所有部件的更新。
  • 优化部件的无效区域收集性能。

2020-02-23

如果 max-width、max-height、min-width、min-height 的值是百分比单位,则需要在容器尺寸确定时更新一次。

2020-02-21

在布局时,如果容器部件的宽度已经确定的,则计算非 fixed 宽度的子元素的宽度,如果容器部件的高度已确定,则计算非 fixed 高度的子元素的高度。

2020-02-16

布局已经重写完毕,接下来是补充单元测试,然后测试 LC Design 的效果是否正常。

2020-02-15

由于 flex 容器里的元素的尺寸会被更新两次,第一次在主动更新的时候,第二次是被动更新布局的时候,前者有样式和布局的差异判断,会触发 resize 事件,而后者并没有这些判断,导致滚动条只响应了目标部件的第一次尺寸更新时的 resize 事件,从而使滚动条尺寸计算错误。

看样子需要将部件的样式和布局差异对比代码独立出来,以供布局系统调用。

absolute 定位的元素,尺寸变化不需要触发父元素的重布局。

滚动条在响应 resize 事件时会纠正滑块的位置,由于 flex 容器在两次 resize 事件触发时的尺寸不一样,滚动条会出现滚不动的情况。看样子要将 resize 事件放到部件更新完后触发了。

改成延迟触发 resize 事件后,手动调整窗口宽度时滚动条位置会有闪动,问题不大,暂不处理。

2020-02-11

flex 布局不能依赖子元素的现有尺寸,否则在其中一个子元素主动更新尺寸后整个布局会被打乱,部件每更新一次,它的尺寸就会被多拉伸/收缩一次。可以给部件加上 max_content_width 和 max_content_height 属性,在部件主动更新布局时保存实际内容尺寸,这样在更新 flex 布局时计算的剩余空间大小就不会被上次布局结果给影响了。

2020-02-08

在 inline-block 元素里嵌套 block 元素时会使 block 元素的尺寸计算出错,针对这一问题,可以添加 max-content 来标识尺寸调整规则,在更新完自身布局后计算内容最大尺寸,供父级 inline-block 计算内容尺寸。但 block 的特性是宽度填满剩余空间,这意味着它需要在父级 inline-block 元素计算完内容区域尺寸后再次调整宽度,那怎么区分这两个阶段的尺寸更新?可以为部件原型添加 layout 方法,它与 autosize 方法的说明如下:

  • autosize: 用于获取自适应的内容区域尺寸
  • resize: 用于在确定自身尺寸后进行布局

在这次改动后,TextView 部件会在 autosize() 被调用时根据尺寸调整规则计算文本内容尺寸,在 resize() 被调用时采用部件自身的内容区域尺寸来重新布局文本。

flex 布局也需要针对这情况做处理,仔细想想还是有点复杂。

重新梳理一遍布局流程:

  1. 样式计算阶段
    • 将尺寸分为以下几类:
      • fixed: 可被直接计算出来的
      • percent: 百分比单位
      • fill: 填充剩余空间
      • fit-content: 自适应内容
      • none: 依赖父级元素布局才能确定尺寸
    • 在部件的尺寸、显示方式、定位变化的时候,添加重布局任务
  2. 布局阶段
    • 宽度是否为 fixed ?
      • 是,采用当前内容区域宽度
      • 否,计算最大内容区域尺寸
        • 遍历子部件
        • 根据子部件的尺寸来计算合适的内容区域宽度
      • 采用实际内容宽度重新布局
        • 遍历子部件
        • 子部件宽度是否为 fixed ?
          • 是,仅更新位置
          • 否,计算实际尺寸,并让子部件重新布局
        • 根据子部件的尺寸来计算内容区域高度
        • 更新绝对定位的子部件的位置和尺寸
    • 如果尺寸变化,则标记父级部件需要重新布局

在样式更新阶段,父级尺寸变化会触发子部件重布局,而布局任务是从子到父进行的,因此,在计算内容区域尺寸时可直接使用子部件的尺寸,无需考虑子部件尺寸受上次布局影响而产生的不稳定的问题。但这一个规则只适用于 block 布局,flex 布局会根据可用空间对子元素做拉伸/收缩处理。

从上述内容可得出布局有三种方式:

  • max-content: 尽可能让内容尺寸最大化。
  • fixed: 采用现有的内容区域尺寸进行布局。若子部件宽度不为 fixed,则会递归进去采用 fixed 方式对子部件重新布局。
  • auto: 如果部件宽度是 fixed,则采用 fixed 方式,否则采用 max-content 方式。该方式在执行布局任务时使用。

2020-02-04

开始改进脏矩形收集功能,先整理一些需求:

  • 脏矩形收集应该高效,能快速处理上万部件
  • 当父级元素已有脏矩形时,子元素无需重复添加
  • 脏矩形应该在所有部件布局完后添加

在更新和布局阶段产生的脏矩形大部分都是基于部件的内容框、画布框创建的,为方便去重,可以用一个变量来标识当前脏矩形大小。解决方案大致如下:

  • 给部件的结构体添加三个成员:
    1. invalid_area:存储当前的脏矩形,坐标相对于父级内容区域
    2. invalid_area_type:标识当前的脏矩形大小,用于子部件的脏矩形去重判断
    3. has_child_invalid_area:标识子部件是否有脏矩形,以决定是否递归遍历子部件列表
  • 调整给部件的脏矩形记录接口:
    1. 如果父级部件的 invalid_area_type 不为 0 则直接返回
    2. 将新的脏矩形与 invalid_area 合并,不另外新增记录
  • 调整更新流程,在每个部件更新完后,如果部件位置和尺寸有变化,则合并变化前后的脏矩形并存储到 invalid_area 中
  • 完成所有更新后,遍历部件树,收集每个部件中的 invalid_area 的值,并重置 invalid_area_type 为 0

这种做法是用部件更新性能换取渲染耗时,在部件数量多且变化频繁的情况下可能会使更新耗时高于渲染耗时,到时候需要增加自定义规则来支持禁用部件内的差异检测和脏矩形功能。

2020-02-02

TextView 的尺寸更新规则有点混乱,在新的布局系统中 autosize() 方法已经改为在布局阶段调用,为了适应新的布局系统,决定对 TextView 做出如下更改:

  • 在样式更新阶段,若文本样式有变化,则会标记需要重新布局。
  • 在布局阶段,根据当前尺寸,重新排版文本,计算文本尺寸。
  • 移除自定义任务,TextView_SetTextW() 方法必须在 UI 线程中调用。

2020-02-01

cpp-coveralls 调用的 gcov 命令找不到源文件,gcov 文件里记录的源文件路径是相对于 Makefile 的,而 LCUI 的每个模块目录都会分配一个 Makefile,这使得 cpp-coveralls 无法准确定位文件。看来只能提交 issue 给 cpp-coveralls 了。

2020-01-31

调整一下尺寸计算规则:

  • 如果尺寸为百分比单位,则在样式更新阶段和在父级容器的布局阶段计算出实际值。
  • 如果尺寸为 px 单位的固定值,则根据现有的内容区域尺寸更新布局。
  • 如果尺寸为 auto
    • 如果父级元素是 flexbox 布局,则根据现有的内容区域尺寸更新布局。
    • 如果是 inline-block 显示方式,则在布局后让尺寸自适应内容尺寸。
    • 如果是 block 显示方式,则在样式更新阶段计算实际值。

从上述规则可以用以下枚举来标识:

  • fixed: 可以被直接计算出来的固定值,例如:100px、100%。
  • fill: 填充可用空间,例如:非 flexbox 容器中,宽度为 auto 的 block 元素。
  • fit-content: 自适应内容大小,例如:绝对定位的元素、非 flexbox 容器中的 inline-block 元素。
  • none: 无需在样式更新阶段计算的值,它们会在父级容器布局完后计算实际值,例如:flexbox 容器中的元素。

Flex 布局已经完成,测试程序覆盖了 flex-grow、flex-shrink、justify-content、align-items、垂直居中布局、圣杯布局,接下来是添加单元测试,然后调整无效区域的收集机制。

Test flex layout

2020-01-30

当 margin 为 auto 时,需要在布局时计算合适的值。在 flexbox 布局开始时,先重置这些 margin 值为 0,避免影响剩余空间的计算。

如果元素的尺寸是百分比单位,则在布局时只将其 min-content-size 算入容器内容尺寸,等容器内容尺寸计算完后再计算这些元素的实际尺寸。

flexbox 容器中的元素被拉伸/收缩后需要给它们更新一次布局,但现在的布局在更新后都会更新容器尺寸,那么该如何避免拉伸/收缩后的尺寸被布局流程重置?

height: 100% 的元素在布局完后,高度会被重置为 min-content-height,这尺寸计算流程有点混乱,样式更新和布局更新阶段都会更新尺寸。

2020-01-29

在 flexbox 容器的布局更新后,如果其中的 TextView 部件有样式更新则会触发重新布局导致尺寸被重置,如何处理?有两种办法:

  1. TextView 部件是 block 或 inline-block 显示方式,在它更新布局后,判断父级是否为 flex 显示方式,是则不更新尺寸。
  2. 当 flexbox 容器中的元素有触发重布局时,给 flexbox 容器也触发重布局。

第一个方法要加判断,比较麻烦,只能选择第二种,现阶段暂不考虑布局性能。

之前提到在更新样式时需要构造一颗树来记录需要更新布局的部件,实际上没必要这样做,因为更新部件样式是从顶到底递归的,可以在更新完子部件样式后再更新布局。

flex-basis 需要改成在布局时计算。

2020-01-26

简单的布局流程如下:

  • 对元素进行分组,遍历时顺便统计 margin: autoflex-shrinkflex-grow 的元素数量及总值
  • 遍历组,计算该组在主轴上的剩余空间
    • 若剩余空间大于 0
      • 根据每个元素的 flex-grow 值来拉伸尺寸
      • 若还有剩余空间,则平均分配给 margin 值为 auto 的元素
      • 若还有剩余空间,则根据 justify-content 的值,给未设置 margin 的元素设置合适的 margin 值
    • 若剩余空间小于或等于 0
      • 根据每个元素的 flex-shrink 值来收缩尺寸

关于剩余空间的分配规则可参考《控制Flex子元素在主轴上的比例》,文档中提到 min-content-szie 和 max-content-size,它们在《CSS Box Sizing Module Level 3》中有相关说明,看上去需要为 LCUI 添加能够计算它们的函数。

2020-01-24

是否需要将 flex 布局相关样式属性存到 widget 数据结构里?

  • 是。好处是方便对比前后差异,判断是否需要重新计算布局,属性访问也方便。坏处是大部分部件都用不到 flex 布局,增加了部件的内存占用。
  • 否。每次部件样式更新时,无论更新的样式属性是否与 flex 布局相关,都需要重新计算布局。

2020-01-23

block 布局系统及相关单元测试已经重写完毕,接下来是 flex 布局。

2020-01-19

在浏览器中,vertical-align 的计算值与基线有关,而基线又受到行高和是否有文字的影响,如果有文字则按照文字基线来计算内联块元素的位置,否则将内联块元素的最下边缘作为基线来计算元素位置,以下是测试效果:

Chrome vertical-align

如果给其中一个内联块元素添加文本,布局会变成这样:

Chrome vertical-align

在 LCUI 中,text-align 和 line-height 是 TextView 部件的私有样式属性,不参与布局系统的坐标计算,目前也不打算完全实现浏览器的这种基于基线的布局方式,所以,LCUI 对 vertical-align 的处理很简单,都基于当前行的实际高度来计算。

LCUI vertical-align

2020-01-12

启用 OpenMP 后 CPU 使用率一直在 80% 以上,即使没有重绘区域也是如此,看来需要根据重绘区域面积来自动决定是否使用 OpenMP。

block 和 inline-block 布局已经重写完成,接下来开始重写布局的单元测试。

2020-01-11

简单的布局流程如下:

  • block
    1. 遍历子元素
    2. 计算子元素的位置
    3. 按行分组,计算每一行的最大高度
    4. 累加所有行的高度,作为容器的高度
  • inline-block
    1. 遍历子元素
    2. 计算子元素的位置
    3. 按行分组,计算每一行的最大宽度和高度
    4. 将最大行宽和所有行的高度之和作为容器的宽高

inline-block 和 block 容器的布局类似,可以共用同一布局代码。

布局使用的是部件的 outer box,当 margin 为负数时,outer box 会比 border box 小。

一次遍历没法完成全部部件的坐标计算,那就遍历两次,第一次只按行分组子部件,计算内容区域的宽高,第二次再根据内容宽高来计算子元素的布局。

对于宽高是百分比单位的元素,可在更新样式时计算出准确的值,因为 root 的宽高必定是固定的,那么就能在更新样式后根据父级最大可用尺寸来计算出当前可用尺寸。假设有个 inline-block 显示方式的容器,如果它已指定宽度或最大宽度,则子元素宽度的百分比是相对于这个容器内容宽度,否则相对于最大可用宽度。

absolute 定位的元素脱离布局流,需要在计算完内容区尺寸后更新这些元素布局。

relative 和 absolute 定位的元素,它们的 left、right、top、bottom 属性的变更不需要重新布局,但考虑到现在并没有频繁改动这些属性的需求,所以就不花时间想优化方案了,直接触发重布局。

2020-01-05

在改动部件更新流程后无法通过布局测试,试着追加了几行 LCUIWidget_Update() 调用,可通过测试,看样子现有的布局效率很低,要往复更新好几次才能让布局完全正确。

准备重新调整部件更新方式,将布局任务与样式更新任务分离,在从根到子级更新部件样式时构造一棵树来记录需要更新布局的部件,更新完样式后再递归进这颗树从子级到根更新布局。

2019-12-29

改进后的重绘效果:

paint flashing

2019-12-22

之前提了个重绘区域高亮的改进任务,看到有贡献者提交了 PR 就顺便测试了一下,以 helloworld 程序为例,鼠标悬停在输入框、 hello, world! 上会触发重绘,点击个按钮都会触发全屏重绘,这些重绘是多余的,为此,决定改进部件的更新流程。

首先,集中处理部件属性的差异对比和无效区域的标记,原有的部件属性更新函数只负责更新样式,不再考虑属性变化后的副作用。然后,将属性的更新分为两类:

  • 布局相关:包括宽高、定位、位置、内间距、外间距等属性在更新时会触发重新布局和重绘。
  • 渲染相关:包括背景、边框、阴影、可见性、透明度等属性在变化时只触发重绘,如果布局相关属性已更新,或父级部件不可见,或父级布局已标记内容区域需要刷新,则可跳过这些属性的差异对比。

这样处理后,就不需要 RectList_Add() 处理重复、重叠的脏矩形了。

2019-12-21

在启用 OpenMP 后,可将屏幕划分为 4 个区域来并行渲染,区域高度最小 200 像素。

当前机器的逻辑处理器是 8 个,将并行渲染线程数设置为 8 个后,渲染性能降低,改成 4 个较为合适。

2019-12-18

部件数量多的时候,存在以下问题:

  • 脏矩形处理耗时长
  • 部件样式更新耗时长

以下是性能探查器的检测结果:

Performance

测试程序在每次改完部件样式后会调用 LCUIWidget_RefreshStyle() 刷新全部部件样式,一般情况下应用程序并不需要主动调用这个函数,而是应该调用 Widget_UpdateStyle() 更新当前改动的部件样式。在调整测试程序后,渲染性能会从 30 帧会降到 5 帧,性能检测结果如下:

Performance

由于 LCUIWidget_RefreshStyle() 会标记全屏为无效区域,使后续添加的其它区域都会被忽略掉,所以花费在脏矩形处理上的时间很少。在改成 Widget_UpdateStyle() 调用后,总共会添加 3600 个脏矩形,而脏矩形的处理方式很粗暴,每次添加脏矩形时会遍历一遍脏矩形记录,导致性能非常低。

文章版权归作者所有,未经许可不得转载。

问题反馈

对此文章有疑问?你可以点击 这里 反馈