大前端开发中的“树” (下)

2021-10-08 10:10:15 浏览数 (1)

本系列文章共分为上、下两篇,介绍 Web、Android、iOS、Flutter 这些前终端平台下,与 “树” 及视图系统有关的技术话题,并尝试分析它们之间的异同点;方便从事大前端开发的同学对各平台的技术特性有更广泛的了解。

  • 视图左上角为坐标原点 (0,0)
  • CGPoint(x, y) 创建坐标点
  • CGSize(width, height) 表示视图宽度和高度
  • CGRect 结合了CGPoint 和 CGSize
  • origin 表示左上角所在的 CGPoint(x, y)
  • bounds 是指在自身视图中的 CGRect(x=0, y=0, width, height)
  • frame 是在父视图的 CGRect(x, y, width, height)
  • center 是指在父视图中的 CGPoint(x width / 2, y height / 2)

iOS 坐标系统概念图 4.3 UIView UIView 负责接收触摸手势事件通过 UIResponder 来响应,负责显示、支持动画效果等则由 CALayer 来支持。 UIView 声明 4.4 事件响应链机制 上面介绍 UIView 负责响应触摸手势等事件有 UIResponder 负责, UIResponder 是 UIView 的父类,主要实现了事件响应链(Responder Chain):当  UI  收到某个信号的响应后这种控件间自上到下消息传递的链路。其中最重要的就是 事件传递流程 以及 如何找到第一响应者。 事件响应链流程图 [2] 4.5 CALayer CALayer 与 UIView 的关系是: [3]

  • UIView 为 CALayer 提供内容,专门负责处理触摸等事件,参与响应链
  • CALayer 全权负责显示内容 (contents)

视图显示原理图 [3] 4.5.1 图层树 CALayer 在概念上与 UIView 类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置,在数据结构上构成树的形式,称之为图层树;图层树的能力包括:

  • 阴影、圆角、带颜色的边框
  • 3D 变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

在 CALayer 的工作过程中,又衍生出了三种树:呈现树、模型树、渲染树。 [4] 4.5.2 呈现树与模型树 呈现树是图层树中所有图层的呈现图层所形成,模型树是所有图层的模型图层所形成。 呈现图层仅在图层首次被提交的时候创建。它的作用是,CALayer 在做隐式动画时,CoreAnimation 就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着 CALayer 除了 “真实” 值(视图描述中设置的值)之外,必须要知道当前显示在屏幕上的属性值,而每个图层属性的显示值都被存储在呈现图层中。 不过,为了让 CoreAnimation 更新显示,大多数情况下不需要直接访问呈现图层,而是通过和模型图层交互即可。典型场景包括同步动画和处理用户交互:

  • 如果是实现一个基于定时器的动画,而不仅仅是基于事务的动画,这个时候需要准确知道在某一时刻图层显示在什么位置,以便正确摆放图层;
  • 如果想让做动画的图层响应用户输入,可以使用 hitTest 方法来判断指定图层是否被触摸,这个时候呈现图层而不是模型图层调用 hitTest 会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

4.5.3 渲染进程与渲染树 动画和屏幕上组合的图层被一个单独的进程管理,而不是应用程序,这个进程就是所谓的渲染服务。 渲染过程会被细分为四个分离的阶段:

  • 布局:准备视图 / 图层的层级关系,以及设置图层属性(位置、背景色、边框等)的阶段
  • 显示:图层的寄宿图片被绘制的阶段
  • 准备:CoreAnimation 准备发送动画数据到渲染服务,同时也是 CoreAnimation 将要执行一些别的事务例如解码动画过程中将要显示图片的时间点
  • 提交:CoreAnimation 打包所有图层和动画属性,然后通过 IPC 发送到渲染服务进行显示

打包的图层和动画到达渲染服务进程,他们会被反序列化来形成叫做渲染树的图层树。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:

  • 对所有的图层属性计算中间值,设置 OpenGL 几何形状(纹理化的三角形)来执行渲染
  • 在屏幕上渲染可见的三角形

五、Flutter 中的树 Flutter 中树的结构和 Web 中的非常相似。本节尝试会它们进行一些类比,同时也会展示 Flutter 中的树实际是如何运行的。 5.1 和其他平台的相似点 在很多资料中都会提及 Flutter 有三颗树 (Widget 树、Element 树、RenderObject 树),这个概念有助于我们从其他平台快速过渡到 Flutter ,我们暂且使用这个概念叙述,后文再探讨 Flutter 中具体的树组织形式。 5.1.1 Widget 本质上是一个配置文件,它决定了节点的配置信息。比如:颜色、图片、文字、控件大小等。它和 Android View、iOS UIView 、 Web HTML CSS 有一定的对应关系。 5.1.2 Element 对比差异减少操作对底层绘制操作次数的中间节点。类比到 Web 就是前文提到的 Virtual DOM,在 Android Composed 和 iOS 的 SwiftUI 中也有相似的概念。 5.1.3 RenderObject 实现 layout、paint 两套协议,确定在 Canvas 在局部位置应该如何绘制。在 Web 的语境下它就相当于 DOM 树,在Android 和 iOS 的语境中它覆盖了 View 中 layout 和 paint 流程。 5.2 运作流程 前文提到,Flutter 的树用三棵树来描述并不完全精确。这小节尝试揭示它的全貌。先上一张总览图: 总览图 可以看到 Flutter 中有四个和视图相关的树形结构 (Widget、Element、RenderObject、Layer),它们之间又相互关联汇总成一棵以 RootElement 为根节点的视图树。接下来将从树的构建以及视图更新两个过程展开描述。 5.2.1 树的构建 在一个 Flutter App 创建的同时会配套地生成三个根节点 (Widget、Element、RenderObject),也就是总览图中标记为红色的节点。紧接着 Flutter 会从 rootElement 出发,触发一次 build 流程。 build 流程由一个 Element 节点触发,首先与它相连的 Widget 节点获取到它的子节点,并用它产生一个 Element 节点和一个RenderObject 节点 (可能没有,根据 Widget 类型决定),新产生的节点会挂载到原先的父节点下。参照下图 build 流程理解起来会更直观一些。 build 流程 接下来将不断的重复这一个过程,直到 Widget 获取不到子节点,树的第一次构建就结束了。最终获得一个类似总览图中显示的数据结构。 5.2.2 视图更新 Flutter 中视图更新有三个类型分别是 build、layout、paint。这三种类型都遵循一个统一的流程,下面用更新流程图展示。 build 流程 某一个节点需要刷新时,会将自己添加到一个单例对象 Owner 的 dirty 列表中,表示自己需要更新。当下次 vsync 信号到来时,Owner 会遍历 dirty 列表中的元素,让它们都重新执行一次对应的步骤。 build 我们在树的构建一节已经提到 build 的流程,视图更新的流程基本一致,区别在于 Element (或者 RenderObject) 此时可能已经存在子节点了,因此在 Widget 创建新对象之前会有一个 Diff 逻辑,以确定是否需要更新。Diff 的逻辑和本系列文章的上篇中提到的 Web Diff 逻辑相似,这里不作详细展开。 layout、paint 在 paint 的流程中会产生若干 Layer (详见 Layer 小节) 并且它们形成一个树的结构,完成了整棵大树的最后拼图。 layout 和 paint 的具体过程与其他平台的处理过程相近,这里不作详细展开。 Layer RenderObject 可以被理解为画布的局部,Layer 则代表在这个局部画布中的一个图层。我们可以通过将图层按顺序叠放起来最终得到想要的图案。它的行为相对较独立,并且主要作用于创建它的 RenderObject ,因此在其他资料的树结构中常常不会提及它。 5.2.3 小结 Flutter 中各个组件构成一整棵树的整体,通过组件间的协同来完成视图的绘制。Widget 暴露给开发者使用,借由它的轻量级允许开发者在数据变化的时候频繁的创建;Element 充当一个过滤网隔绝不必要的变化;RenderObjcet 藏在最底层处理页面的绘制。 六、总结 本节尝试从共性特征、实现对比和演进过程的角度,加以总结。 共性特征

  • “树” 作为视图元素层级化的组织形式,普遍存在于各个前端视图系统中。
  • 前端视图系统均基本遵循 解析视图描述布局渲染 的处理过程。此外,在解析视图描述环节,通过引入 Virtual DOM 类 “逻辑树” ,可以有效增强视图变更性能。

实现对比:窥探平台间性能差异 在各具体平台下,树结构携带的信息及其对渲染结果的影响程度不完全相同。 以动画系统为例,iOS 的视图系统把动画配置作为视图树描述的一部分,直到渲染时才计算实际值,从而提升动画性能;而 Android 渲染过程一般依靠视图树的变化实现动画,相比之下增加了处理环节。这在一定程度上反映了 iOS 和 Android 设计思路的差异,或许也可以作为早期 iOS 动画性能优于 Android 的佐证。 演进过程:Virtual DOM 思想的开枝散叶 自 React 引入 Virtual DOM 开始,维护一个 “抽象的视图描述”,成为近代 Web 开发的主流方案。Virtual DOM 通过屏蔽上层代码对 DOM 的直接维护,可以实现更可控的局部更新,从而提升性能和易用性。 而 Flutter 的视图系统进一步实践了这个思想:通过 Widget - Element 树的工作机制,筛选变化、减少操作,支撑高性能渲染。更进一步,上层业务代码可以在 Widget 声明 “有状态” 和 “无状态” 来显式控制更新。 “来自 React 框架的设计灵感” [5] 同时,在 Apple 的 SwiftUI 和 Google 的 Jetpack Compose 这两个新一代视图方案中,同样引入了视图状态的概念和局部视图更新能力,不妨也看作对 Virtual DOM 思想的致敬。 参考资料 [1] iOS 坐标系统  https://cloud.tencent.com/developer/article/1330805 [2] iOS 技术总结 - UI 触摸事件与事件响应  http://roadmap.isylar.com/iOS/UIKit/UIResponder.html [3] iOS 技术总结 - iOS UIView 刷新与渲染机制http://roadmap.isylar.com/iOS/UIKit/UIViewRender.html [4] iOS 核心动画高级技巧 (中文翻译) https://github.com/qunten/iOS-Core-Animation-Advanced-Techniques [5] Flutter Architectural Overview - Rendering and Layout https://flutter.dev/docs/resources/architectural-overview#flutters-rendering-model QQ音乐招聘 Android / iOS 客户端开发,点击左下方“查看原文”投递简历~ 也可将简历发送至邮箱:tmezp@tencent.com

0 人点赞