Flutter 2 渲染原理和如何实现视频渲染

7 月 17 日下午,在前端专场巡回沙龙北京站中,声网Agora跨平台开发工程师卢旭辉带来了《Flutter2 渲染原理和如何实现视频渲染》 的主题分享,本文是对演讲内容的整理。

本次分享主要包括 3 个部分:

  1. Flutter2 概览。
  2. Flutter2 视频渲染插件的实践。
  3. Flutter2 渲染原理(源码)。

前言

其实 Flutter1 在国内的占有率并不算高,很多开发者可能知道 Flutter 的上层语言是基于 Google 的 Dart (一个曾经企图取代 JavaScript 的语言,但最后以失败告终),而Dart语言也是很多开发者不太能接受Flutter的点。国内很多公司可能还是选用 ReactNative 或者坚持原生开发,不过伴随着 Flutter2 的问世(全平台支持),以及阿里的北海框架(基于 Flutter Engine 的渲染能力实现的上层使用 JavaScript 的跨平台框架),我相信 Flutter2 未来可期。考虑到很多读者可能是前端开发者,所以在第三部分我会以 Web 的视角切入,大家会看到很多熟悉又陌生的内容,是不是 Flutter 开发者或者是否了解 Flutter 都不重要,重要的是 Flutter 的设计思想,希望对大家有所帮助。

Flutter2 概览

Flutter2 是 Google 在 2021 年 3 月份发布的 Flutter 最新版本,它基于 Dart1.12 支持了 Null-Safety (空安全检查),大家可以类比TypeScript的"?",编译器会要求你对可能为空的数据进行校验,这样可以在开发过程中避免一些空指针的问题。而更为重要的就是对 Web 端提供了稳定版的支持,对桌面端的支持也已经合入。

下面我们一起看下 Flutter2 的整体架构:

Flutter 2 渲染原理和如何实现视频渲染_第1张图片

Flutter2 的 Web 部分包括 Framework 层和 Browser 层,其中 Framework 层涵盖渲染、绘制、手势处理等,Browser 层涵盖 CSS、HTML、Canvas、WebGL 等(毕竟还是在浏览器上运行),而最后的 WebAssembly 是为了使用 C 和 C++ 从而调度 Skia 渲染引擎,这个我们在第三部分也会详细介绍。

Native 部分除了通用的 Framework 层,还包括 Engine 层 和Embedder 层,其中 Engine 层主要包括 Dart 虚拟机、Isolate 的初始化,还有图层合成、GPU 渲染、平台通道、文本布局等,而 Embedder 层主要用于不同平台的特性适配。

乍一看,Web 和 Native 的差异还是挺大的,但其实 Web 这边也有一个基于 Dart 开发的 Engine 层,叫作 web_ui,主要用来处理 Web 上的 Composition 和 Rendering 等。

Flutter 2 渲染原理和如何实现视频渲染_第2张图片

接下来简单看一下 Flutter2 的平台差异,如上图所示。目前 Flutter2 支持 6 个主流平台,分别是 Web、Android、iOS、Windows、macOS 和 Linux。对比其他的跨平台框架,比如 ReactNative 和 Electron (分别是移动端和桌面端的代表),Flutter2 有着更为丰富的平台支持,虽然 ReactNative 也有微软贡献的桌面端支持,以及 expo 对 Web 的支持,但还不够统一。

对于一些构建工具或包管理工具, Flutter2 使用了各个平台比较标准的方式,比如 Web 还是基于 JavaScript,这得利于 dart2js 将 Dart 编译为 JavaScript;在 Android 中还是基于 Gradle 体系;在 iOS 和 macOS 中是基于 CocoaPods 把 Flutter 引入工程中;在 Windows 和 Linux 中则主要是基于 CMake。

关于 Flutter 的一些特性,比如 PlatformView,它提供了桥接原生控件的能力,比如在 Web 上显示一个 Element 或者在 Android、iOS 上显示自定义的 View。不过目前桌面端暂不支持 PlatformView,这并不是说技术上无法实现,而是目前还未开发。ExternalTexture 是外接纹理,用户可以对自己的图形数据进行渲染。dart::ffi 使 Flutter 拥有直接调用 C 和 C++ 的能力,这两点除了 Web 都是支持的。

Flutter2 视频渲染插件的实现

1、渲染视频插件实现流程

接下来将分享下声网在视频渲染插件方面的实践,这里主要针对 Web 和桌面端。

Flutter 2 渲染原理和如何实现视频渲染_第3张图片

就像在前面平台差异中所描述的那样,Web 不支持 ExternalTexture,Desktop 不支持 PlatformView。所以在 Web 上我们通过 PlatformView 的方式去实现视频渲染,基本的流程是使用 ui.platformViewRegistry 注册 PlatformView 并返回 DivElement,在 DivElement 创建完成之后,需要使用 package:js 实现 Dart 和 JavaScript 的互相调用。

声网有专门的 Web 音视频 SDK,所以我们并没有在 Dart 层做过多的操作,而是做了 JS 层的包装,由这个包装库来调度 SDK 操作 WebRTC 以创建 VideoElement,最后 append 到先前创建的 DivElement 中实现视频渲染。

Flutter 2 渲染原理和如何实现视频渲染_第4张图片

接下来看一下桌面端的方案,因为它不支持 PlatformView,所以想实现自定义的视频渲染,我们只能使用 ExternalTexture 方案,通过 MethodChannel 调用 Native 层中自定义的 createTextureRender 函数,由它调度 FlutterTextureRegistry 创建 FlutterTexture,同时 将textureId 抛回 Dart 层与 Texture Widget 绑定。Native SDK 的视频数据会在 AgoraRtcWrapper 层进行图像格式转化,然后我们可以通过 FlutterTextureRegistry 的 MarkTextureFrameAvailable 函数通知 FlutterTexture 从回调中获取图像数据。

2、Flutter2 开发中遇到的一些坑

在插件开发过程中我们也会遇到一些问题,这里给大家简单分享一下:

Flutter 2 渲染原理和如何实现视频渲染_第5张图片

就桌面端而言,macOS 是 OC 头文件,Windows 是 C++ 的头文件。Linux 则是 C 的头文件,这部分并没有完全统一,甚至有些 API 都不一样,所以在桌面开发过程中会遇到很多麻烦,毕竟它目前也没有完全稳定。

具体举一些案例,如上图所示,前面 3 个都是在 Web上遇到的问题。

1.ui.platformViewRegistry在Web上会报错,是因为它并没有在Framework层的ui.dart中定义,而是定义在web_ui/ui.dart中,不过它并不影响运行,所以可以选择使用ignore注释忽略它。

2.我们使用 dart::js,比如构建一个 JavaScript 对象,这时候会使用 @JS 的注解进行声明,如果没有加上external构造函数,虽然在 Debug 模式下能够正常运行,但在 Profile 和 Release 模式下会报错。

3.dart::io 主要用来做一些具体平台的调用,比如平台判断在 Web 上是无法使用的。我们可以使用 if(dart.library.html) 在 import 的时候指向自定义的 Dart 文件,并对相关 API 定义空实现,也可以使用 kIsWeb 在 Web 上不去执行相关 API。

4.在 Windows 上,是使用 EncodableValue 来进行 Dart和C++ 的通信(基于 C++17 的 std::variant,可以理解成 TypeScript 中的 type1|type2|type3)。在处理 int32 和 int64 的时候,Framework 层直接判断是不是超过 int32最大值,如果超过则直接标注成 int64,有用过声网 SDK 的开发者可能会知道,我们的 用户ID的类型是 uint32,uint32 取值范围有部分区间大于 int32 并小于 int64,因此如果单纯使用 std::get 来获取,则不论指定 int32_t 还是 int64_t 都有可能报错,好在它提供了 LongValue 函数,在内部做好了判断并统一使用 int64 返回。

接下来是本次主题的重点 Flutter2 渲染原理,Flutter 引擎这部分有很多原理是通用的,只不过在 Web 上用 Dart 实现,在 Native 上则主要使用 C 和 C++ 实现。

Flutter2 的渲染原理

1、Flutter Framework

在正式开始前,我们先简单回顾一下,之前提到 Flutter 框架分为 Framework 部分和 Engine 部分,而渲染流程也是这两个部分互相配合完成的,但是区别于其他框架由上层处理完后直接交给下层的特点,Flutter Engine 会提供一些 Builder 供 Framework 使用,所以很多流程都由这两个部分来回调度完成的。

Flutter 2 渲染原理和如何实现视频渲染_第6张图片

先看一下Flutter的整个渲染流程,UserInput 是处理用户输入,Animation 是动画,不过这两个部分不是今天要探讨的重点,Build 主要用于使 Widget 生成 Flutter 框架能识别的 RenderObject,Layout 主要用于确定组件位置和尺寸等,Paint 主要用于转化渲染对象为 Layer,再由 Composition 进行合并,最后 Rasterize 光栅化进行 GPU 渲染。

Flutter 在处理 UI 时都是基于树形结构,从下图中我们可以看到 3 个树形结构,分别是 Widget Tree、Element Tree 和 Render Tree。

Flutter 2 渲染原理和如何实现视频渲染_第7张图片

我们从 Widget 开始,创建一个 Container,其中包含 Row ( Flex 布局容器),而 Row 又包含 Image 和 Text。Container 内部包含 ColoredBox,它可以作为背景或者边框。Image 内部包含 RawImage,Text内部则包含了 RichText,只有 ColoredBox、Row、RawImage 和 RichTexth 才会被转为 RenderObjectElement,它们最终会分别生成对应的 RerderObject。

那么我们看一下 RenderObject 是什么,它是真正需要被渲染的对象,其中的 attach 函数会把渲染的流程交给 PipelineOwner 管理,下图中 3 个函数主要用于判断是否需要 Layout、是否需要被合成,以及是否需要绘制。

Flutter 2 渲染原理和如何实现视频渲染_第8张图片

现在看一下PipelineOwner的主要功能,它用于管理渲染流程,首先 Flutter 初始化时会注册一个帧回调,Flutter 的帧是由其自身管理的,随即会在回调中触发 flushLayout、flushCompositingBits 和 flushPaint这 3 个函数,它们和之前提到的 RenderObject 的 3 个 mark 函数相对应。

Flutter 2 渲染原理和如何实现视频渲染_第9张图片

PipelineOwner 中有 3 个数组,之前被 mark 的 RenderObject 会分别存放在这个 3 个数组中,最后 flush 的时候可以快速遍历这些 RenderObject。经过 PipelineOwner 处理之后,它会调用 RenderView 的 compositeFrame 函数,这部分我们会在后文做讲解。

我们先来重点看下 flushPaint 函数,flushPaint 会调用 RenderObject 的 paint 函数,这是一个抽象函数,它本身是没有实现的,而是由继承它的子类去实现。

Flutter 2 渲染原理和如何实现视频渲染_第10张图片

可以看到 paint 函数的第一个参数是 PaintingContext,我们来看一下它的部分 API,它们的返回值都是 Layer,包括后面的 pushClipRect 等函数会分别返回 Layer 的不同子类。所以 paint 函数的一个职责就是将 RenderObject 转成 Layer,并将其添加到其成员的 ContainerLayer 中,顺带一提,这里的 LayerHandle 是一个引用计数,用来处理自动释放。

Flutter 2 渲染原理和如何实现视频渲染_第11张图片

而 paint 函数的另一个职责就是对于需要绘制的 RenderObject,通过 PictureRecorder 将 Canvas 的绘制指令保存起来。

Flutter 2 渲染原理和如何实现视频渲染_第12张图片

Canvas 主要用于绘制需要绘制的对象,比如之前提到的 RichText、RawImage 等,除此之外,还可以进行 transform、clipPath 等操作。

Flutter 2 渲染原理和如何实现视频渲染_第13张图片

这里的 Canvas 工厂构造中,会判断 useCanvasKit 并构造不同的 Canvas,为什么会有这个逻辑,这里先按下不表,后面会介绍。我们先按照 Render Pipeline 往下看。

之前提到的 PipeLineOwner 流程结束后,会调用 RenderView 的 compositeFrame 函数进行 Layer 合成。而在 compositeFrame 函数中,我们可以看到几个非常重要的 Class,那就是 Scene 和 SceneBuilder,Scene 是 Layer 合成完毕后的产物,由 SceneBuiler 构建得到。

Flutter 2 渲染原理和如何实现视频渲染_第14张图片

如图所示,最后它会调用 _window.render 函数,这里的 _window 是 SingletonFlutterWindow,它是一个单例的 RenderView,后面会详细介绍,我们先看一下 Build Scene 的流程。

Flutter 2 渲染原理和如何实现视频渲染_第15张图片

这里我们可以看到 Layer 的部分源码,之前提到 RenderObject 中有一个 ContainerLayer,buildScene 就是调用 ContainerLayer 的 buildScene 函数(如上图的右半部分),随后会调用 Layer 的 addToScene 函数,它和 RenderObject 的 paint 函数类似,也是一个抽象函数,需要 Layer 的子类自己去实现,比如 ContainerLayer 的 addToScene 函数就是遍历 Child Tree 来分别调用子 Layer 的 addToScene。

Flutter 2 渲染原理和如何实现视频渲染_第16张图片

那 addToScene 做了什么呢,它实际上是调用 SceneBuilder 提供的 pushXXX 函数,这些函数的返回值也是 Layer,只不过是 EngineLayer,Layer 是 Framework 中图层的抽象,而EngineLayer 是 Engine 中图层的抽象,随后在 Engine 层将这些 EngineLayer 组合到 Scene 中。

2、Flutter Engine

Framework 层已经介绍得差不多了,接下来我们来看一下 Engine 层。

简单回顾一下,我们的 Widget 会经由这样的转换流程:Widget->RenderObject->Layer->EngineLayer->Scene,那么这个 Scene 如何渲染出来呢?

Flutter 2 渲染原理和如何实现视频渲染_第17张图片

这里我们看到了之前提到的 SingletonFlutterWindow,它的 render 函数会调用 EnginePlatformDispatcher 的 render 函数,这里我们又看到了熟悉的 useCanvasKit,根据判断将 Scene 强转成了不同的 Scene,那么这个 useCanvasKit 到底表示什么呢,我们接着往下看。

这个时候我们必须得引入一个概念,就是 Web Renderer,在 Flutter Web 中有两种渲染模式:一种是基于 HTML 标签的渲染模式,它会将 Flutter 的 Widget 都映射成不同的标签,无法单纯用标签表示的就会使用 Canvas 进行绘制,有点类似于 ReactNative 的表现形式。

另一种则是基于 CanvasKit 的渲染模式,它会下载 2MB 的 wasm 文件以调用 Skia 渲染引擎,Widget 渲染都是通过该引擎来绘制的。

Flutter 2 渲染原理和如何实现视频渲染_第18张图片

我们可以通过命令行参数在 flutter build 或者 run 的时候指定渲染模式,值得一提的是,默认的渲染模式是 auto,在桌面端浏览器上默认是 CanvasKit,而在移动端 WebView 上默认是 HTML。

首先,我们来看一下 HTML 渲染模式,以 我们 Flutter SDK 的 API Example 为例,通过 Elements Tree 可以看到,它的标签层级还是比较多的,图片中的 标签指向了 "Basic" 的文本,这说明该模式下文本的渲染使用的是 Canvas,那为什么要使用 Canvas 绘制文本而不使用浏览器默认的文字渲染能力呢?那是因为要抹除平台渲染表现的差异,尤其是文字的换行处理等,Flutter 内置了文字排版的引擎,会基于该引擎进行渲染。此处延伸一下,比如输入框组件,在没有获取焦点的状态下,它其实和 Text 是类似的,如果获取了焦点 Flutter 则会添加一个 标签,然后接收输入的文字信息,当焦点失去的时候再隐藏,这是一个非常巧妙的方案。

Flutter 2 渲染原理和如何实现视频渲染_第19张图片

接下我们看一下在 HTML 渲染模式下的一些细节。之前按下不表的 Canvas 在这里就要显示它的真身了,在HTML渲染模式下会构建 SurfaceCanvas,可以从右图中看到List,这就是存放绘图指令的集合。

Flutter 2 渲染原理和如何实现视频渲染_第20张图片

而对于 SceneBuilder,这里的是其子类 SurfaceSceneBuilder,我们可以先看一下下图中右侧的PersistedSurface。

Flutter 2 渲染原理和如何实现视频渲染_第21张图片

它是 EngineLayer 的子类,并且拥有一个 rootElement 属性,还有一个 visitChildren 函数,这也是一个抽象函数。PersistedLeafSurface 是一个没有 child 的EngineLayer,所以它的 visitChildren 是空实现,由它派生出 PersistedPicture 和 PersistedPlatformView,分别对应图片文字(我们之前提到文字是使用 Canvas 绘制的)和平台 View。PersistedContainerSurface 就是一个容器的 EngineLayer,它也有非常多的子类,比如 PersistedClipPath、PersistedTransform等,这些 EngineLayer 对应到之前 API Example 复杂的 Elements Tree 中的各个自定义标签。

在 SurfaceSceneBuilder 的 build 函数执行后,生成的 SurfaceScene 中的 webOnlyRootElement 就已经包含了我们的整个 Html Element 了。

最后我们可以看到 SurfaceScene 会调用 DomRenderer 的 renderScene 函数,将这些 Element 添加到 _sceneHostElement 中。

Flutter 2 渲染原理和如何实现视频渲染_第22张图片

到这里 HTML 渲染模式就完结了。

下面我们看一下 CanvasKit 的渲染模式,从 Elements Tree 中我们可以看到该模式下的层级非常简单,所有的渲染都是在一个 canvas 中进行的,这里用到的 #shadow-root 是 HTML 的一个特性,可以做到样式隔离。

Flutter 2 渲染原理和如何实现视频渲染_第23张图片

同样,我们先从 Canvas 入手,这里的是 CanvasKitCanvas,而绘图指令则保存在 CkPictureSnapshot 的 _commands 属性中。

Flutter 2 渲染原理和如何实现视频渲染_第24张图片

对于 SceneBuilder,CanvasKit 渲染模式下的子类是 LayerSceneBuilder,这里的 Layer 类似于 HTML 渲染模式下的 PersistedSurface,都是派生自 EngineLayer,并且有用一个 ContainerLayer 包含所有的 child,也有对应的 PictureLayer 和 PlatformViewLayer。不过不同的是,它有一个 paint 函数,这里的 paint 函数才是真正的操作 GPU 进行绘制的函数。

而 LayerSceneBuilder 的 build 函数生成的 LayerScene 中包含一个叫作 LayerTree 的根节点,和 HTML 渲染模式下的 webOnlyRootElement 相对应。

Flutter 2 渲染原理和如何实现视频渲染_第25张图片

既然这里提到 paint 函数才是真正的绘制,那么我们来看一下它是什么时候被调用的。

之前提到 How To Render Scene 的时候,LayerScene 通过调用 rasterizer 的 draw 函数进行绘制。Rasterizer 是负责光栅化进行 GPU 渲染的类,这里会先调用 acquireFrame 从 LayerTree 中获取 frameSize 以构建 SurfaceFrame,同时也会在其内部构建 SkSurface,绑定 WebGLContext 等一系列对 Skia 的调度操作。

context.acquireFrame 生成的 Frame 只是一个简单的聚合类,不用太在意,随后调用 Frame 的 raster 函数进行光栅化处理。最后的 addToScene 则是将 baseSurface 中的 canvas 的 HTML 标签添加到 skiaSceneHost 中。

Flutter 2 渲染原理和如何实现视频渲染_第26张图片

光栅化阶段由 preroll 和 paint 组成,分别计算绘制边界,以及遍历 LayerTree 并调用所有 Layer 的 paint 函数,这里的 PaintContext 区别于 Framework 的 PaintingContext,它持有所有的 canvas,以便于不同的 Layer 对其进行 paint 操作。

Flutter 2 渲染原理和如何实现视频渲染_第27张图片

至此,CanvasKit 渲染模式下的流程也差不多走完了,我们最后看一下最终是如何显示在HTML 中的。其实,CanvasKit 渲染模式下最终也使用了 DomRenderer,在 Flutter 的初始化流程中,我们可以看到,initializeCanvasKit 函数的前半部分是我们之前提到的引入 Skia 的 wasm 资源和对应的 JavaScript 文件;后半部分则是创建了一个 skiaSceneHost 根节点,这个 Element 就是之前 baseSurface.addToScene 中引用的。

Flutter 2 渲染原理和如何实现视频渲染_第28张图片

整个渲染原理到这里就介绍完了,当然,整个渲染中还有很多的细节,比如 SurfaceFactory 中除了 baseSurface 还有 backupSurface 可以对绘制进行缓存等,这些点每个展开都能作为一个单独的议题进行讨论。最后贴上一个总结的流程图,大家可以结合前文回顾一下整个流程。

Flutter 2 渲染原理和如何实现视频渲染_第29张图片

在分享的最后,给大家附上 Flutter RTC SDK 的 GitHub 链接,目前我们已经在 dev/flutter 分支上做了 Flutter2 的适配。在 Web 和桌面端上也支持了屏幕共享。大家可以自行体验,如果有任何问题或者建议,欢迎大家反馈,如果使用体验还不错,也欢迎大家给我们的仓库点上 Star。

Flutter 2 渲染原理和如何实现视频渲染_第30张图片

你可能感兴趣的:(flutter音视频)