本文介绍 Runtime 层的保留层缓存。它是一项底层优化,不需要修改组件 API。
脏区重绘可以缩小重绘区域,但不会减少该区域内需要重新提交的静态图元数量。
保留层缓存用于减少后一项成本:
静态子树图元 -> 离屏图层纹理
脏区重绘 -> 绘制已缓存的图层纹理 + 动态图元
这对 Gallery 一类复杂页面很有用,因为一次按钮 hover 可能与大量静态卡片、阴影和文本绘制段相交。
当前实现有意采用保守策略:
- Runtime 自动选择静态子树和相邻的稳定兄弟节点。
- 两个及以上相邻且符合条件的兄弟节点,可以在不改变绘制顺序的前提下合并为一个保留绘制段图层。
- 组件不需要主动启用,也不对外暴露缓存 API。
- OpenGL 将每个保留层存储为纹理支持的 framebuffer。
- Vulkan 将每个保留层存储为可采样的 color-attachment image 和 framebuffer,并使用预乘 alpha 合成。
- 不支持保留层的后端会安全回退到普通图元重放。
- Backdrop blur 和 dependent visual 子树不会被缓存。它们会切断兄弟节点绘制段,但前后的合格静态兄弟节点仍可分别形成独立绘制段。
- 包含动画、交互、滚动、timer、frame callback、dirty key、image 或 SVG 的子树不会被缓存。
- 子树的绘制成本和面积必须达到阈值才会进入缓存。
- 候选内容必须连续稳定两帧,Runtime 才会创建图层纹理。
缓存键包含结构、绘制边界、绘制成本、DPI scale,以及会影响绘制结果的元素属性。
首版 MVP 上线后,Runtime 在渲染热路径中保留候选检查的时间过长。这对复杂静态页面影响不大,但会给主要由叶子图元组成的高动画负载 demo 带来额外开销。
当前流程如下:
layout()或子树重建时,在Element上缓存静态阻断标记。update()在PaintBoundsInstance中缓存子树当前是否存在活动动画。render()只读取这些缓存标记,并跳过仅包含叶子节点的保留层探测。
这样既能让优化保持在底层,又能避免每帧重复递归扫描子树。
渲染脏区
遍历已排序的子节点
收集每一段相邻且符合条件的稳定兄弟节点
如果绘制段至少包含两个兄弟节点,并通过合并后的成本/面积检查:
使用或创建一个保留绘制段图层
否则逐个处理子节点:
如果子树已有有效保留层:
在 dirty/scissor 裁剪下绘制图层纹理
否则,如果子树是缓存候选:
创建保留层,并在本帧正常绘制该子树
否则:
正常绘制子树
创建单个子树图层或兄弟节点绘制段图层时,都会在该次创建过程中禁用嵌套保留层复用,避免出现意外的嵌套 framebuffer 状态。
新建或重建的图层不会在创建它的同一帧中被采样。Runtime 会先将图层标记为有效,让子树或兄弟节点绘制段在本帧继续走普通图元路径,并请求下一帧完整绘制。后续帧会从稳定状态同时接管 render cache 和保留层;此后,未变化的静态内容会继续命中保留层缓存,带 transition 能力的静态 UI 也不会因此永久禁用缓存。
窗口标题中的渲染统计包含:
Layer H/M/D/Re
H:保留层缓存命中次数。M:缓存未命中或候选内容不稳定的次数。D:图层纹理绘制次数。Re:图层纹理重建次数。
复杂静态页面上的正常按钮交互,应表现为较高的 H、较低的 Re,以及更少的图元绘制次数。
- 兄弟节点绘制段只合并相邻且符合条件的子节点;阻断节点会切开绘制段,Runtime 不会跨过它们重新排序或合并。
- 为保证正确性,当前不会缓存继承了活动 transform 的内容。
- Backdrop blur 需要采样已有 framebuffer 内容,因此不会进入缓存。
- 它不是完整的 retained scene graph。
Rect批处理是独立的后端优化,并非由该缓存提供的跨图元通用 batch renderer。 - Vulkan 会尽可能在 swapchain 重建期间保留图层纹理,并延迟重建依赖 render pass 的 framebuffer。