
从架构到源码:一文了解 Flutter 渲染机制-阿里云开发者社区 (aliyun.com)
Flutter 从本质上来讲还是一个 UI 框架,它解决的是一套代码在多端渲染的问题。在渲染管线的设计上更加精简,加上自建渲染引擎,相比 ReactNative、Weex 以及 WebView 等方案,具有更好的性能体验。本文将从架构和源码的角度详细分析 Flutter 渲染机制的设计与实现。
- widget 配置表,它只是一个配置数据结构[{text: ““}{textcolor: “}, {backgroud:””}],存放渲染内容,创建是非常轻量的,在页面刷新的过程中随时会重建
- Element UI 节点,同时持有 Widget 和 RenderObject,存放上下文信息,通过它来遍历视图树,支撑 UI 结构,widget 更改数据结构交由系统更新 element
- RenderObject: 根据 Widget 的布局属性进行 layout,paint,负责真正的渲染
flutter 启动 build,把 widget 转换成 Element ,再转换成 RenderObject
1)向 Android 系统注册并等待 VSync 信号
Flutter 引擎启动时,会向 Android 系统的 Choreographer(管理 VSync 信号的类)注册并接收 VSync 信号的回调。
2)接收到 VSync 信号,通过 C++ Engine 向 Dart Framework 发起渲染调用
当 VSync 信号产生以后,Flutter 注册的回调被调用,VsyncWaiter::fireCallback() 方法被调用,接着会执行 Animator::BeiginFrame(),最终调用到 Window::BeginFrame() 方法,WIndow 实例是连接底层 Engine 和 Dart Framework 的重要桥梁,基本上与平台相关的操作都会通过 Window 实例来连接,例如 input 事件、渲染、无障碍等。
3)Dart Framework 开始在 UI 线程执行渲染逻辑,生成 Layer Tree,并将栅格化任务 post 到 GPU 线程执行
Window::BeiginFrame() 接着调用,执行到 RenderBinding::drawFrame() 方法,这个方法会去驱动 UI 界面上的 dirty 节点(需要重绘的节点)进行重新布局和绘制,如果渲染过程中遇到图片,会先放到 Worker Thead 去加载和解码,然后再放到 IO Thread 生成图片纹理,由于 IO Thread 和 GPI Thread 共享 EGL Context,因此 IO Thread 生成的图片纹理可以被 GPU Thread 直接访问。
4)GPU 线程接收到 Layer Tree,进行栅格化以及合成上屏的工作
Dart Framework 绘制完成以后会生成绘制指令保存在 Layer Tree 中,通过 Animator::RenderFrame() 把 Layer Tree 提交给 GPU Thread,GPU Thread 接着执行栅格化和上屏显示。之后通过 Animator::RequestFrame() 请求接收系统的下一次 VSync 信号,如此循环往复,驱动 UI 界面不断更新。
widget-》element -》renderObject -> layout -> paint ->layer -> GPU
用 key 加速 Flutter 的性能优化,本质是减少 element 的创建
- 一个 element 是由 Widget 内部创建的,它的主要目的是,知道对应的 Widget 在 Widget 树中所处的位置。但是元素的创建是非常昂贵的,通过 Keys(ValueKeys 和 GlobalKeys),我们可以去重复使用它们。
- GlobalKey 是全局使用的 key,在跨小部件的场景时,你就可以使用它去刷新其它小部件。但,它是很昂贵的,如果你不需要访问 BuildContext、Element 和 State,应该尽量使用 LocalKey。
- LocalKey 包括 ValueKey 和 ObjectKey、UniqueKey,无法跨容器使用,ValueKey 比较的是 Widget 的值,而 ObiectKey 比较的是对象的 key, UniqueKey 则每次都会生成一个不同的值。
为了去改善性能,你需要去尽可能让 Widget 使用 Activie 和 Update 操作,并且尽量避免让 Widget 触发 UnMount 和 Mount。使用 GlobayKeys 和 ValueKey 则能做到这一点。
repaintBoundry
什么是三棵树
在 Flutter 中和 Widgets 一起协同工作的还有另外两个伙伴:Elements 和 RenderObjects;由于它们都是有着树形结构,所以经常会称它们为三棵树。
Widget:Widget 是 Flutter 的核心部分,是用户界面的不可变描述。做 Flutter 开发接触最多的就是 Widget,可以说 Widget 撑起了 Flutter 的半边天;
Element:Element 是实例化的 Widget 对象,通过 Widget 的 createElement() 方法,是在特定位置使用 Widget 配置数据生成;
RenderObject:用于应用界面的布局和绘制,保存了元素的大小,布局等信息;
初次运行时的三棵树
内在的协同关系
1 | class ThreeTree extends StatelessWidget { |
由三个 Widget 组成:ThreeTree、Container、Container。那么当 Flutter 的 runApp()方法被调用时会发生什么呢?
当 runApp()被调用时,第一时间会在后台发生以下事件:
- Flutter 会构建包含这三个 Widget 的 Widgets 树;
- Flutter 遍历 Widget 树,然后根据其中的 Widget 调用 createElement()来创建相应的 Element 对象,最后将这些对象组建成 Element 树;
- 接下来会创建第三个树,这个树中包含了与 Widget 对应的 Element 通过 createRenderObject()创建的 RenderObject;
下图是 Flutter 经过这三个步骤后的状态:
从图中可以看出 Flutter 创建了三个不同的树,一个对应着 Widget,一个对应着 Element,一个对应着 RenderObject。每一个 Element 中都有着相对应的 Widget 和 RenderObject 的引用。可以说Element 是存在于可变 Widget 树和不可变 RenderObject 树之间的桥梁。Element 擅长比较两个 Object,在 Flutter 里面就是 Widget 和 RenderObject。它的作用是配置好 Widget 在树中的位置,并且保持对于相对应的 RenderObject 和 Widget 的引用。
三棵树的作用
简而言之是为了性能,为了复用 Element 从而减少频繁创建和销毁 RenderObject。因为实例化一个 RenderObject 的成本是很高的,频繁的实例化和销毁 RenderObject 对性能的影响比较大,所以当 Widget 树改变的时候,Flutter 使用 Element 树来比较新的 Widget 树和原来的 Widget 树:
1 | //framework.dart |
- 如果某一个位置的 Widget 和新 Widget 不一致,才需要重新创建 Element;
- 如果某一个位置的 Widget 和新 Widget 一致时(两个 widget 相等或 runtimeType 与 key 相等),则只需要修改 RenderObject 的配置,不用进行耗费性能的 RenderObject 的实例化工作了;
- 因为 Widget 是非常轻量级的,实例化耗费的性能很少,所以它是描述 APP 的状态(也就是 configuration)的最好工具;
- 重量级的 RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用;
在框架中,Element 是被抽离开来的,每个 Widget 的 build(BuildContext context)方法中传递的 context 就是实现了 BuildContext 接口的 Element。
更新时的三棵树
因为 Widget 是不可变的,当某个 Widget 的配置改变的时候,整个 Widget 树都需要被重建。例如当我们改变一个 Container 的颜色为橙色的时候,框架就会触发一个重建整个 Widget 树的动作。因为有了 Element 的存在,Flutter 会比较新的 Widget 树中的第一个 Widget 和之前的 Widget。接下来比较 Widget 树中第二个 Widget 和之前 Widget,以此类推,直到 Widget 树比较完成。
1 | class ThreeTree extends StatelessWidget { |
Flutter 遵循一个最基本的原则:判断新的 Widget 和老的 Widget 是否是同一个类型:
如果不是同一个类型,那就把 Widget、Element、RenderObject 分别从它们的树(包括它们的子树)上移除,然后创建新的对象;
如果是一个类型,那就仅仅修改 RenderObject 中的配置,然后继续向下遍历;
在我们的例子中,ThreeTree Widget 是和原来一样的类型,它的配置也是和原来的 ThreeTreeRender 一样的,所以什么都不会发生。下一个节点在 Widget 树中是 Container Widget,它的类型和原来是一样的,但是它的颜色变化了,所以 RenderObject 的配置也会发生对应的变化,然后它会重新渲染,其他的对象都保持不变。
注意这三个树,配置发生改变之后,Element 和 RenderObject 实例没有发生变化。
上面这个过程是非常快的,因为 Widget 的不变性和轻量级使得他能快速的创建,这个过程中那些重量级的 RenderObject 则是保持不变的,直到与其相对应类型的 Widget 从 Widget 树中被移除。
当 Widget 的类型发生改变时
1 | class ThreeTree extends StatelessWidget { |
和刚才流程一样,Flutter 会从新 Widget 树的顶端向下遍历,与原有树中的 Widget 类型进行对比。
因为 FlatButton 的类型与 Element 树中相对应位置的 Element 的类型不同,Flutter 将会从各自的树上删除这个 Element 和相对应的 ContainerRender,然后 Flutter 将会重建与 FlatButton 相对应的 Element 和 RenderObject。
当新的 RenderObject 树被重建后将会计算布局,然后绘制在屏幕上面。Flutter 内部使用了很多优化方法和缓存策略来处理,所以你不需要手动来处理这些。