第 50 期 - WebF 中的渲染机制与 JS Binding 解析
摘要
文章主要介绍 WebF 基于 Flutter 的浏览器内核相关内容,包括渲染机制、JS Binding、双向通信中的内存管理等,还对比了 WebF 与 WebView 以及 WebF 与纯 Widget 的渲染机制。
一、WebF 整体架构相关
1. 整体架构特点
- WebF 基于 Flutter Framework 上层扩展 Web 能力,不修改 Flutter Engine。它可被看作是 Flutter 插件,通过在 pubspec.yaml 文件中依赖即可使用。
- 动画、手势、绘制等渲染功能复用 Flutter 能力,并且增强了 RenderObject 渲染能力以实现 CSS 布局和绘制,支持构建 DOM Tree。
- WebF 通过 Dart FFI 和 JSEngine 通信,扩展 QuickJS 能力来支持 Web API。
2. Web 渲染能力的关键设计
- 在 WebF 的 Web 渲染能力中,一个 HTML DIV 对应一个 RenderObject,每个 RenderObject 实现 CSS 盒模型。
- 这一设计的原因与 Flutter Widget 的布局能力拆分、高性能渲染有关。Flutter Widget 将布局细分,遵循特定原则达到高性能渲染,如精准控制 UI 变更重建 Widget 范围、通过传递布局约束确定重新 Layout 边界等。若采用组合不同布局能力的 RenderObject 来完成 CSS 计算,工作量大且有损性能,所以采用一个 HTML DIV 对应一个 RenderObject 的设计思路。
二、JS Binding 相关
1. 理解 JS Binding
- 借助 WidgetsFlutterBinding 的 Binding 概念,可将 JS Binding 理解为胶水层。在 WidgetsFlutterBinding 中,它负责 Flutter Framework 和 Flutter Engine 之间的交互,如 Framework 层向 Engine 请求渲染一帧的调用关系。
- 在 WebF 中,对于 C ++ DOM 对象,JS Binding 桥接 QuickJS 和 C ++ DOM API 的实现,胶水代码通过代码生成脚本来实现,脚本输入源是 Typescript 的 AST 数据(定义 Web 标准 API 接口);对于 Dart DOM 对象,JS Binding 桥接 Dart DOM 对象和 JS DOM 对象的双向通信逻辑。
三、自定义 HTML Element 的 Demo 相关
1. 自定义 HTML Element 的能力概览
- Dart 业务若想通过自定义 Widget 作为 HTML Element 嵌入到 Web DOM Tree,接入方法流程简单。首先要继承抽象类 WidgetElement,然后按需使用多种能力,如实现 build 方法返回内嵌 Widget、注册 Widget 和 HTML Elements 名字的映射、实现 initializeProperties 接口定义 HTML Elements 属性的 set 和 get 方法、实现 initializeMethods 接口定义 HTML Elements 方法对应的调用、决定方法是异步还是同步、Dart 侧通过 dispatchEvent 接口向 JS 侧发送单向通知等。
- 在 JS 侧使用由 Flutter Widget 实现的 HTML Elements 也很方便,就像使用 Web Components 一样,可直接创建 HTML Elements、调用 Dart 侧定义的属性和方法、监听 Dart 侧事件、设置 CSS 属性等。
- WebF 在提供原生组件给 H5 使用的机制上比移动端 WebView 强,如支持 H5 和原生同层渲染、JS API 通信耗时短(基于 Dart FFI 通信减少 JSON 字符串拷贝和序列化耗时)等。
2. WebF 对比 WebView 同层渲染
- iOS:基于移动端 WebView 实现同层渲染依赖 WKWebView 的隐式实现,存在功能无法使用或异常无法修复的风险。如设置 DOM 节点 CSS 属性为 overflow: scroll 时,客户端需遍历 WKWebView 子 View 数组找到对应 DOM 节点添加原生视图。
- Android:App 需接入 Chromium 内核使用 WebPlugin 机制结合 embed 标签实现同层渲染,未接入则无法使用。而 Flutter Engine 支持 Embedder 层的 Platform View 和 Widget 在同一个 Surface 上渲染,并且有多种优化,如尽量复用 Widget 渲染结果、避免 Widget 渲染阻塞、有众多开源原生内嵌组件等。基于这些优点,结合 WebF 的能力,在技术路线上能提供比移动端系统自带 WebView 更好的体验。
- 微信和手淘也在往自渲染引擎的小程序方向发展,如微信小程序框架使用 Flutter Engine 替换 WebView 渲染、微信小程序在性能等方面用基于 Flutter Engine 的 Skyline 比基于 WebView 渲染更优秀。
3. Demo 代码和录屏展示
- 给出了一个简单的 HTML Demo 代码,其中 flutter - button 是由 Flutter Widget 实现的 HTML Element,包含设置初始配置、属性的 set 和 get 方法、添加事件回调等逻辑。
- 以 FlutterButton 为例介绍了 Dart 业务自定义 HTML Element 时的关键实现,如继承 WidgetElement 并重写 build、initializeProperties、initializeMethods 等方法,在这些方法中实现相关功能,如在 initializeProperties 中设置属性的 setter 和 getter 函数,在 initializeMethods 中定义函数等。
四、Dart FFI 内存分配器和 Struct 使用相关
1. 内存分配器
- 在 Dart FFI 中,不同系统平台使用不同的内存分配器管理 Dart 与 C/C ++共享的 heap 内存。POSIX 系统使用 malloc 和 calloc 配合 free 来释放内存,Windows 系统使用 ole32.dll 中的 CoTaskMemAlloc 和 CoTaskMemFree 进行分配和释放。在 WebF 场景下,出于性能考虑,Dart FFI 分配共享 heap 内存使用 malloc。
2. Struct 使用
- 在 dart:ffi library 提供了 Struct class,可在 Dart 中定义与 C 结构体兼容的内存布局,Dart 和 C 之间通过传递 Struct 的指针共享内存。
- 通过定义 C Struct、将 C 函数绑定到 Dart 函数、Dart 为 C Struct 分配内存并传递给 C 的示例,展示了 Dart 与 C 之间的交互过程,其中要注意数据类型匹配和内存对齐方式等问题。
五、NativeBindingObject 是 JS 和 Dart 双向通信基础相关
1. NativeBindingObject 的定义与创建时机
- 在 Dart 侧,NativeBindingObject 继承 Dart Struct,包含多个外部变量,其中 invokeBindingMethodFromDart(Dart 主动调用,C ++实现)和 invokeBindingMethodFromNative(C ++主动调用,Dart 实现)是双向通信的关键函数。
- 在 C ++侧也有 NativeBindingObject 的定义,通常由 WebF C ++侧负责创建,创建时机是 C ++侧 BindingObject Class 的构造函数里。C ++和 Dart 的 DOM 对象都继承 BindingObject,通过 NativeBindingObject 实现双向通信。
2. 双向通信过程
- 以 FlutterButton 为例,当响应点击事件时,Dart 会通过 invokeBindingMethodFromDart 将点击事件发送到 JS。JS 执行相关代码时,会通过胶水层调用到 C ++实现的 DOM 对象,C ++的 DOM 对象再调用 Dart 侧的 invokeBindingMethodFromNative 找到对应的 Dart DOM 对象执行相应任务。
- 以 JS 代码 createButton.type 的执行过程为例详细介绍了 JS 调用 Dart,Dart 再将返回值回调给 JS 的过程,包括 QuickJS 通过胶水层调用 C ++的 DOM 对象、C ++的 DOM 对象调用 Dart 分发任务、invokeBindingMethodFromNative 参数设置和解析、Dart 侧执行实际任务并设置返回值等步骤,其中涉及到 NativeValue Struct 等重要数据结构的操作。
六、双向通信涉及的内存管理相关
1. QuickJS 内存管理优化
- QuickJS 支持基于引用计数的 GC 管理,创建和使用 JS 对象时需要手动管理引用计数。WebF 基于 QuickJS 扩展 DOM 对象时,在创建 WebF Page 对应的 ExecutingContext 构造函数里注入 DOM 类,有多项优化措施。
- 优化 1:用代码生成脚本生成 QuickJS 胶水代码,按照 TypeScript 定义的接口生成胶水代码,避免重复工作和错误导致的内存问题。
- 优化 2:要求每个 C ++ DOM 对象继承 ScriptWrappable 并通过特定模板函数创建,自定义 GC 标记行为和 JS Object 析构行为,避免 JSValue 意外释放和内存泄漏等问题。
- 优化 3:使用 mimalloc 代替 C malloc,减少小对象导致的内存碎片,提高单线程内的内存操作性能。
2. Dart_Handle 内存管理
- Dart_Handle 由 Dart GC 管理内存,如果要在多个作用域使用,需要用 Dart_DeletePersistentHandle 接口持久化并成对使用释放引用。在 WebF 的 Demo 中,Dart 调用 C 时传递 Dart 回调函数指针和 Dart Object,Dart Object 在 C ++侧管理内存时,涉及到持久化和解持久化操作,遵循谁创建谁释放的原则。
3. JSRuntime 内存管理小优化
- 在 WebF 支持 JS Runtime 独立线程模式下,发现了一些问题,如默认会触发 UI 线程共用的 JS Runtime 初始化但不被使用,Flutter Engine 销毁后存在内存泄漏。
- 解决方案是将 UI 线程共用的 JS Runtime 生命周期管理对齐 JS 独立线程,和 WebF Page 生命周期绑定,并且使用正确的 Dart API 管理 NativeFinalizer 代替之前的方法,避免了 DartContext 循环引用等问题。
七、UICommands 驱动 Flutter 更新渲染相关
1. UICommand 的高性能设计
- 介绍了 UICommandItem 结构体的定义,其固定为 32 个字节,在 64 位系统上 CPU 读取更高效。结构体成员包含 UI 命令枚举值、参数字符串及其长度、要执行命令的对象指针、执行命令的参数值等,Dart 侧按照一定逻辑解析。
- UICommand 相比移动端 WebView 和原生通信有诸多优点,如数据载体 C Struct 的高效设计、Dart 侧解析无需 JSON 序列化和值拷贝、命令参数值类型无需额外检查、缓冲池支持多种触发更新渲染策略等。
2. DartMethodPointer 的注入时机
- 在 DartContext 构造函数时通过 Dart FFI 创建 C ++ DartIsolateContext 实例并传入包含相关函数地址指针的参数,DartMethodPointer 在初始化 Bridge 时由 Dart 通过 Dart FFI 传递到 C ++的 DartIsolateContext 管理,在 DartMethodPointer 构造函数里解析函数地址。
3. Dart 侧如何处理 UICommand
- C ++侧执行完相关 JS 代码添加 UICommand 到缓冲池后调用 DartMethodPointer::requestBatchUpdate 请求渲染一帧,Dart 侧的处理时机是在 WebF 的特定回调中执行 flushUICommand 函数,该函数内部会读取、解析、执行 UICommand,更新 DOM 对象的 CSS 属性并利用 Flutter 渲染机制触发渲染。
4. JS 代码到 Dart 触发渲染完整流程
- 介绍了从 JS 代码更新 CSS 属性到 Dart 标记 RenderObject 脏并触发渲染,再到 Flutter Engine 监听 VSync 信号回调执行渲染计算的完整流程,包括 JS 代码执行时的调用栈、C ++和 Dart 侧的操作、Flutter Engine 的回调处理等。
5. WebF 和纯 Widget 渲染机制的简单对比
- 以响应用户点击改变 Widget 偏移值为例,Flutter Widget 业务开发中只需一次 VSync 回调,而 WebF 需要两次 VSync 回调,但基于 UICommand 的高性能设计,仍可能比移动端 WebView 组件性能高,且 Flutter Engine 支持同层渲染而 WebView 组件没有官方公开接口支持。
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有