Flutter鸿蒙PlatformView同层渲染实现深度解析

Viewed 0

关于 Flutter 的 PlatformView 混合开发,我们在过去已经讨论过多次,尤其是在 Android 平台,它已经拥有了 VD、HC、TLHC、HCPP 等多种兼容实现。而本文将深入探讨 Flutter 在鸿蒙平台的 PlatformView 实现,核心在于解析其如何实现“同层渲染”。

同层渲染

Flutter 是一个自渲染的跨平台框架,在鸿蒙平台,它通过 XComponent 支持 skia 和 Impeller 渲染,XComponent 提供了一个用于渲染的 Surface(NativeWindow)。在鸿蒙中,XComponent 可以直接获取到底层的 OHNativeWindow 实例,然后通过扩展 VK_OHOS_surface 将窗口转为 Vulkan 中的 VKSurface,进而通过 VKSwapchain 实现窗口绘制。

接下来重点讨论鸿蒙平台 Flutter 的 PlatformView 如何实现“同层渲染”,简单来说,就是将 ArkUI 渲染到 Flutter 内部,类似于 ArkUI 中将控件渲染到 Web 组件的机制。

事实上,在鸿蒙官方 ArkUI 的 Web 组件中,可以通过开启 enableNativeEmbedMode 来启用 WebView 的“同层渲染”。其基本原理是:底层使用空白的 H5 页面,用 Embed 标签占位,ArkTS 使用 NodeContainer 占位,最后将 Web 侧的 surfaceId 与原生组件绑定,使原生组件渲染到 Web 中。

具体步骤包括:

  1. Stack 组件层叠 NodeContainer 和 Web 组件,并开启 enableNativeEmbedMode 模式。
  2. 封装一个继承 NodeController 的控制器(如 SearchNodeController)。
  3. Web 组件加载 nativeembed_view.html 文件,解析到 Embed 标签后,通过 onNativeEmbedLifecycleChange 接口上报创建消息。
  4. 在回调中根据 embed.status,将配置传入控制器后执行 rebuild 方法,重新触发 makeNode
  5. makeNode 触发后,NodeContainer 获取 BuilderNode 对象,BuilderNode 承载了原生控件的纹理,从而使页面显示原生组件。

核心在于通过 NodeContainer 占位,并实现其对应 NodeControllermakeNode 方法,将原生控件绘制到 BuilderNode,而 BuilderNode 通过 surfaceId 关联到一个可绘制区域。代码示例如下:

makeNode(uiContext: UIContext): FrameNode | null {
    this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId, type: this.renderType });
    if (this.componentType === 'native/component') {
      this.rootNode.build(wrapBuilder(searchBuilder), { width: this.componentWidth, height: this.componentHeight });
    }
    return this.rootNode.getFrameNode();
  }

@Builder
function searchBuilder(params: Params) {
  SearchComponent({ params: params })
    .backgroundColor($r('app.color.ohos_id_color_sub_background'))
}

因此,关键组件是 NodeContainerBuilderNode,它们是 ArkUI 上混合开发的基础。Flutter 在鸿蒙的 PlatformView 实现也类似:通过 BuilderNode 导出 ArkUI 控件的纹理,导出的纹理在 XComponent 中实现“同层渲染”,这与 Flutter 在 Android 上的 VD 实现较为接近。

各种 Node

首先需要了解 BuilderNode 是什么。BuilderNode 在 ArkUI 中是一个自定义声明式节点,支持采用无状态的 UI 方式,可以通过全局自定义构建函数 @Builder 定制组件树。定制得到的 FrameNode 节点,可以直接由 NodeController 返回并挂载于 NodeContainer 节点下。

BuilderNode 还提供了组件预创建的能力,例如可以结合它提前对 ArkWeb 组件进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过 NodeController 动态挂载与显示。

通过 BuilderNode,我们也能理解 NodeContainer 的作用:用于挂载自定义节点(如 FrameNodeBuilderNode),并通过 NodeController 动态控制节点的上树和下树。组件接受一个 NodeController 的实例接口,因此 NodeContainer 需要和 NodeController 组合使用。

严格来说,NodeContainer 仅支持挂载自定义节点 FrameNode,对于 BuilderNode,实际上是获取它的根节点 FrameNode

ArkUI 和 Flutter 类似,也有三棵树:Component Tree、Element Tree 和 RenderNode Tree,它们的作用与 Flutter 三棵树基本一致,其中 RenderNode Tree 是存在于 C++ 后端引擎中的最终渲染结构。

FrameNode 可以视为三棵树中 Component Tree 的特殊实体节点,与自定义占位容器组件 NodeContainer 配合,可以在占位容器内构建一棵自定义的节点树。

FrameNode 作为特殊 Component 节点,提供了节点创建和删除的能力,即在 ArkUI 这种声明式开发场景中,提供了命令式操作的支持。此外,FrameNode 还提供了 getRenderNode 接口,用于获取 FrameNode 中的 RenderNode,从而直接提供绘制的渲染节点。

简单总结:

  • FrameNode + NodeContainer 提供自定义节点支持,通过 FrameNode 提供 RenderNode
  • BuilderNode 提供构建支持和纹理导出,并通过 getFrameNode 获取对应的 FrameNode 对象。

因此可以分类:

  • BuilderNode 是一个自定义的声明式节点。
  • FrameNode 是一个自定义组件节点。
  • RenderNode 是一个自渲染节点。

Flutter

Flutter 鸿蒙在鸿蒙平台的实现方式接近于 Android 平台的 VD,即通过 NodeContainer 挂载节点,并实现事件传递,最终将提取的纹理合并到 Flutter 内进行渲染。

对应到 Flutter 鸿蒙实现中,是 EmbeddingNodeController 的实现。它通过继承 NodeContainer 并实现 makeNode 来创建和管理 ArkUI 的 BuilderNode,而 BuilderNode 中的 wrappedBuilder 则来自封装好的 PlatformView 里的 ArkUI 的 @Builder 实现。

另一方面,EmbeddingNodeController 作为 NodeController 的具体实现,用于管理 NodeContainer。在 Flutter 鸿蒙中,创建和加载 NodeContainer 的对象是 DynamicView它是一个基于 DVModel 数据驱动的对象,可以在 FlutterPage 的默认实现中看到其身影

简单来说,鸿蒙 Flutter 的页面默认在一个 Stack 下:

  • XComponent 提供 surface 绘制,是 Flutter 的渲染画板。
  • 基于 this.rootDvModel 列表的 DynamicView,主要提供 PlatformView 所需的 NodeContainer

这里 DynamicView 基于 DVModel 实体作为驱动,而 DVModel 的存在,是为了用鸿蒙 ArkUI 的声明式范式来管理和渲染由 Flutter 发出的 PlatformView 命令式 UI 操作。因为 ArkUI 是纯粹的声明式 UI 框架,不能像传统 Android 命令式编程那样直接调用 parent.addView(child),而是需要通过状态驱动,让框架根据新状态重新渲染 UI。

DVModel 就是这个“状态”,它是一个用 @Observed 装饰的可观察树状数据结构,用纯数据完整描述整个界面布局,包括哪个位置应该有一个 PlatformView、大小、参数等。

DVModel 作为状态发生变化时,相关的 DynamicView 也会发生变化,这是 Flutter 鸿蒙在 PlatformView 实现上的特殊之处。例如,在鸿蒙 Flutter 中运行 webview_flutter 后,在 ArkUI Inspector 中可以看到布局:通过 DynamicView 构建了一个 NodeContainer,而这里的 NodeContainer 通过 BuilderNode 承载了 Web 组件的纹理。

当存在两个 Web 组件时,控件树里就会有两个 DynamicView,这对应前面 FlutterPage 中基于 this.rootDvModel 列表的实现。如果堆叠两个 Web 并在其上添加一个 Flutter 红色控件,通过 ArkUI Inspector 可以看到对应 DynamicViewNodeContainer 存在的节点,但该节点的内容并没有渲染在原来的位置。

由于 Web 组件通过纹理方式渲染到 Flutter Engine 中,在事件触摸上,触摸事件需要从 Dart 层发出,经过中转,最后通过 EmbeddingNodeControllerpostEvent 转发到 BuilderNode,这点与 Flutter Android 的 VD 模式类似。当然,与 VD 不同的是,因为 Web 是真实存在的节点,键盘输入不会像 VD 那样有太多的连接问题,从这点看又类似 TLHC 实现。

接下来是 Dart 如何触发 PlatformView 构建的实现对象:PlatformViewsChannelPlatformViewsController

当用户在 Dart 层使用 OhosViewOhosViewSurface 创建鸿蒙 PlatformView 时,会触发 PlatformViewsChannelcreate。虽然和 Android 一样存在两个入口,可以根据 Dart 层是否配置了 hybrid 来决定使用 createForPlatformViewLayer 还是 createForTextureLayer,但实际上目前只有 createForTextureLayer 一种可用。如果配置了 hybrid 模式,运行后会发现 view_embedder 为空。

createForPlatformViewLayer 在 Android 走的是 HC 的实现,它把自己作为一个合成边界,渲染路径分叉,把 Flutter 内容渲染到离屏缓冲区,然后通过 SurfaceFlinger 将这些缓冲区和独立渲染的原生视图组合。但在鸿蒙 Flutter 的实现中,关于 createForPlatformViewLayer 的 HC 并没有实现,因此实际上只有 createForTextureLayer 纹理合成这一种 PlatformView 场景。

对于 createForTextureLayer,流程会来到 PlatformViewsController 对象,核心流程包括:

  • 创建一个 platformView,即调用 Plugin 中开发者 PlatformViewFactory 的实现。
  • 通过 Engine 注册得到一个与 Flutter Engine 关联的 Surface id。
  • 创建 EmbeddingNodeController,关联 Surface id 和关联 platformView 的 ArkUI 控件。
  • 创建 DVModel,添加到队列驱动创建 DynamicView

获取 surface id 时,是在 Engine 底层利用系统 Graphic2D 的 NativeImage 能力,通过 OH_NativeImage_Create 创建一个 OH_NativeImage 实例。OH_NativeImage 支持将数据和 OpenGL 纹理对接,或由开发者自行获取 buffer 进行渲染处理。

注意 OH_NativeImage_SetOnFrameAvailableListener,会在 Frame 数据可用时触发 MarkTextureFrameAvailable,这类似于在 C++ 层面将纹理对象标记为“脏”或“过时”,类似 setState 的作用。此操作会触发两个 TaskRunner 的工作:

  • 在 Raster 线程触发设置 texture 为“脏”。
  • 在 UI 线程执行 ScheduleFrame(false),false 表示 Widget 完全相同,Engine 可以跳过整个构建/布局/绘制过程,只需获取最后生成的 layer tree 并在光栅线程上重新渲染。

回到主流程,在得到 DVModel 后,DynamicView 会构建出 NodeContainer,从而触发 EmbeddingNodeControllermakeNode,进而 BuilderNode 构建并提取 PlatformView 里的 ArkUI 控件纹理,最终渲染出画面。

具体到 webview_flutter 中,WebViewPlatformView 继承了 PlatformView,并实现了 getView() 方法,返回 OhosWebView

getView(): WrappedBuilder<[Params]> {
  return new WrappedBuilder(WebBuilder);
}

@Builder
export function WebBuilder(params: Params) {
  OhosWebView({
    params: params,
    webView: params.platformView as WebViewPlatformView,
    controller: (params.platformView as WebViewPlatformView).getController()
  })
}

这个 getView(): WrappedBuilder 会在 EmbeddingNodeController 中被获取,并在 makeNode 里由 BuilderNode 使用,从而实现最终的纹理提取和渲染。

最后,整个 PlatformView 的整体流程核心仍在 NodeContainerBuildNode 的基础上展开,然后基于 DVModel 驱动 DynamicView 更新,进而 makeNode 构建出纹理,触发 Engine 更新 Texture 区域实现绘制。

0 Answers