深入理解 Flutter 的 PlatformView 如何在鸿蒙平台实现混合开发

关于 Flutter 的 PlatformView 混合开发,我们在过去聊了很多次,特别是 Android 平台的 PlatformView ,现在已经同时具备 VD、HC、TLHC、HCPP 等多种兼容实现,甚至我们还深入对比过 Flutter 和 Compose 在 PlatformView 的差异,感兴趣的可以通过下方链接回顾:

而本次我们要深入聊的,则是 Flutter 在鸿蒙平台的 PlatformView 实现,核心是聊聊它是如何实现"同层渲染"

同层渲染

我们知道,Flutter 是一个自渲染的跨平台框架,在之前的《为什么跨平台框架可以适配鸿蒙,它们的技术原理是什么?》我们就聊过了 Flutter 如何适配到鸿蒙,其中就有:

在绘制支持上,现在鸿蒙版 Flutter 已经支持了 skia 和 Impeller 渲染,核心是通过 XComponent 支持,XComponent 提供了一个用于渲染的 Surface(NativeWindow)。

在鸿蒙里,XComponet 可以直接获取到系统底层的 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、因为要使用 NodeContainer ,所以封装一个继承 NodeControllerSearchNodeController 对象
  • 3、使用 Web 组件加载 nativeembed_view.html 文件,Web 组件解析到 Embed 标签后,通过onNativeEmbedLifecycleChange 接口上报 Embed 标签创建消息通知到应用侧
  • 4、在步骤 3 的回调内,根据 embed.status,将配置传入 searchNodeController 后,执行 rebuild 方法重新触发 Controller 的 makeNode 方法
  • 5、makeNode方法触发后,NodeContainer 组件会获取到 BuilderNode 对象,BuilderNode 承载了对应原生控件的纹理,页面出现原生组件

看着是不是有点抽象?实际上其实就是通过 NodeContainer 占位,然后实现它对应 NodeControllermakeNode 方法,把原生控件绘制到 BuilderNode ,而 BuilderNode 通过 surfaceId 关联到一个可绘制区域

kotlin 复制代码
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 还提供了组件预创建的能力,比如通过结合 BuilderNode,可以将 ArkWeb 组件提前进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过 NodeController 动态挂载与显示。

所以,通过 BuilderNode ,我们也了解到了 NodeContainer 的作用:

用于挂载自定义节点(如 FrameNodeBuilderNode ),并通过 NodeController 动态控制节点的上树和下树,组件接受一个 NodeController 的实例接口,所以 NodeContainer 需要和 NodeController 组合使用。

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

是不是看到各种 XXXNode 又有点懵? 其实 ArkUI 和 Flutter 一样,首先 ArkUI 也有三棵树:Component Tree、Element Tree 和 RenderNode Tree ,它们各种的作用也和 Flutter 三棵树基本一致,其中 RenderNode Tree 就是存在于C++后端引擎中的最终渲染结构:

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

FrameNode 作为特殊 Component 节点,它提供了节点创建和删除的能力,也就是在 ArkUI 这种声明式开发场景里,提供了命令式操作的支持 ,另外 FrameNode 还提供了 getRenderNode 接口,用于获取 FrameNode 中的 RenderNode ,也就是通过 FrameNode 可以直接提供绘制的渲染节点,简单来说:

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

所以可以简单分类下:

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

Flutter

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

而这对应到 Flutter 鸿蒙实现里,就是 EmbeddingNodeController 的实现,它通过继承 NodeContainer 并实现 makeNode创建和管理 ArkUI 的 BuilderNode ,而 BuilderNode 里的 wrappedBuilder 则是来自封装好的 PlatoformView 里的 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 和原生 Android XML 不同在于,它是纯粹的声明式 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 ,并且在 Web 上在加一个 Flutter 红色控件,通过 ArkUI Inspector 我们可以看到对应 DynamicViewNodeContainer 存在的节点,只是该节点的内容并没有渲染在原来的位置:

而因为此时 Web 组件是通过纹理的方式被渲染到 Flutter Engine 里,所以在事件触摸上,触摸事件需要从 Dart 层发送出来,经过中转,最后通过 EmbeddingNodeControllerpostEvent 转发到 BuilderNode ,从这点看,也是和 Flutter Android 的 VD 模式类似:

当然,和 VD 不同的是,因为 Web 是真实存在的节点,所以键盘输入不会像 VD 那样有太多的 connection 问题,从这点看又类似 TLHC 实现。

那么到这里我们知道了通过 EmbeddingNodeControllerDynamicView ,如何创建和渲染 ArkUI 控件实现"同层渲染",最后一步就是 Dart 如何触发 PlatformView 构建的实现对象:PlatformViewsChannelPlatformViewsController

当用户在 Dart 层使用 OhosView 或者 OhosViewSurface 创建鸿蒙 PlatformView 时,就会触发 PlatformViewsChannelcreate :

此时虽然和 Android 一样存在两个入口,可以根据 Dart 层是否配置了 hybrid 来决定使用 createForPlatformViewLayer 还是 createForTextureLayer ,但是实际上目前只有 createForTextureLayer 一种可用,如果配置了 hybrid 模式,运行后就会发现此时会出现 view_embedder 为空的情况:

createForPlatformViewLayer 在 Android 走的是 HC 的实现,它和 TextureLayer 相反,它把自己作为一个合成边界,它的渲染路径是分叉的,把 Flutter 内容被渲染到离屏缓冲区,然后通过 SurfaceFlinger 将这些缓冲区和独立渲染的原生视图组合在一起,Flutter 的 UI 控件会 以 PlatformViewLayer 的前后交际关系,被渲染到不同的 Surface 上。

但是目前在鸿蒙 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 并在光栅线程上重新渲染它

上述就是 TextureLayer 的大致注册和渲染流程,回到主流程上,最后流程在得到 DVModel 后, DynamicView 会构建出来 NodeContainer ,从而触发 EmbeddingNodeController makeNode ,进而 BuilderNode 构建并提取 PlatformView 里的 ArkUI 控件纹理,最终渲染出画面。

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

csharp 复制代码
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 区域实现绘制。

参考链接

相关推荐
小白64023 小时前
前端梳理体系从常问问题去完善-网络篇
前端·网络
Mintopia3 小时前
🧙‍♂️ Next.js 权限区分之术:凡人 vs 管理员
前端·后端·全栈
过往入尘土3 小时前
深入探索现代前端开发:从基础到架构的完整指南
前端·人工智能·算法·前端框架
浅蓝色3 小时前
flutter平台判断后续
flutter·harmonyos
比老马还六3 小时前
Blockly串口积木开发
前端
小奋斗3 小时前
浏览器原理之详解渲染进程
前端·面试
伶俜monster4 小时前
搞定 Monorepo,工程能力升级,升职加薪快人一步
前端·架构
猪哥帅过吴彦祖4 小时前
Flutter 系列教程:常用基础组件 (下) - `TextField` 和 `Form`
前端·flutter·ios
Mintopia4 小时前
🌍 跨语言 AIGC:Web 国际化内容生成的多语种模型技术
前端·javascript·aigc