共计 8841 个字符,预计需要花费 23 分钟才能阅读完成。
7 月 17 日下午,在前端专场巡回沙龙北京站中,声网 Agora 跨平台开发工程师卢旭辉带来了《Flutter2 渲染原理和如何实现视频渲染 》的主题分享,本文是对演讲内容的整顿。
本次分享次要包含 3 个局部:
- Flutter2 概览。
- Flutter2 视频渲染插件的实际。
- 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 的整体架构:
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 等。
接下来简略看一下 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 和桌面端。
就像在后面平台差别中所形容的那样,Web 不反对 ExternalTexture,Desktop 不反对 PlatformView。所以在 Web 上咱们通过 PlatformView 的形式去实现视频渲染,根本的流程是应用 ui.platformViewRegistry 注册 PlatformView 并返回 DivElement,在 DivElement 创立实现之后,须要应用 package:js 实现 Dart 和 JavaScript 的相互调用。
声网有专门的 Web 音视频 SDK,所以咱们并没有在 Dart 层做过多的操作,而是做了 JS 层的包装,由这个包装库来调度 SDK 操作 WebRTC 以创立 VideoElement,最初 append 到先前创立的 DivElement 中实现视频渲染。
接下来看一下桌面端的计划,因为它不反对 PlatformView,所以想实现自定义的视频渲染,咱们只能应用 ExternalTexture 计划,通过 MethodChannel 调用 Native 层中自定义的 createTextureRender 函数,由它调度 FlutterTextureRegistry 创立 FlutterTexture,同时 将 textureId 抛回 Dart 层与 Texture Widget 绑定。Native SDK 的视频数据会在 AgoraRtcWrapper 层进行图像格式转化,而后咱们能够通过 FlutterTextureRegistry 的 MarkTextureFrameAvailable 函数告诉 FlutterTexture 从回调中获取图像数据。
2、Flutter2 开发中遇到的一些坑
在插件开发过程中咱们也会遇到一些问题,这里给大家简略分享一下:
就桌面端而言,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 的整个渲染流程,UserInput 是解决用户输出,Animation 是动画,不过这两个局部不是明天要探讨的重点,Build 次要用于使 Widget 生成 Flutter 框架能辨认的 RenderObject,Layout 次要用于确定组件地位和尺寸等,Paint 次要用于转化渲染对象为 Layer,再由 Composition 进行合并,最初 Rasterize 光栅化进行 GPU 渲染。
Flutter 在解决 UI 时都是基于树形构造,从下图中咱们能够看到 3 个树形构造,别离是 Widget Tree、Element Tree 和 Render Tree。
咱们从 Widget 开始,创立一个 Container,其中蕴含 Row(Flex 布局容器),而 Row 又蕴含 Image 和 Text。Container 外部蕴含 ColoredBox,它能够作为背景或者边框。Image 外部蕴含 RawImage,Text 外部则蕴含了 RichText,只有 ColoredBox、Row、RawImage 和 RichTexth 才会被转为 RenderObjectElement,它们最终会别离生成对应的 RerderObject。
那么咱们看一下 RenderObject 是什么,它是真正须要被渲染的对象,其中的 attach 函数会把渲染的流程交给 PipelineOwner 治理,下图中 3 个函数次要用于判断是否须要 Layout、是否须要被合成,以及是否须要绘制。
当初看一下 PipelineOwner 的次要性能,它用于治理渲染流程,首先 Flutter 初始化时会注册一个帧回调,Flutter 的帧是由其本身治理的,随即会在回调中触发 flushLayout、flushCompositingBits 和 flushPaint 这 3 个函数,它们和之前提到的 RenderObject 的 3 个 mark 函数绝对应。
PipelineOwner 中有 3 个数组,之前被 mark 的 RenderObject 会别离寄存在这个 3 个数组中,最初 flush 的时候能够疾速遍历这些 RenderObject。通过 PipelineOwner 解决之后,它会调用 RenderView 的 compositeFrame 函数,这部分咱们会在后文做解说。
咱们先来重点看下 flushPaint 函数,flushPaint 会调用 RenderObject 的 paint 函数,这是一个形象函数,它自身是没有实现的,而是由继承它的子类去实现。
能够看到 paint 函数的第一个参数是 PaintingContext,咱们来看一下它的局部 API,它们的返回值都是 Layer,包含前面的 pushClipRect 等函数会别离返回 Layer 的不同子类。所以 paint 函数的一个职责就是将 RenderObject 转成 Layer,并将其增加到其成员的 ContainerLayer 中,顺带一提,这里的 LayerHandle 是一个援用计数,用来解决主动开释。
而 paint 函数的另一个职责就是对于须要绘制的 RenderObject,通过 PictureRecorder 将 Canvas 的绘制指令保存起来。
Canvas 次要用于绘制须要绘制的对象,比方之前提到的 RichText、RawImage 等,除此之外,还能够进行 transform、clipPath 等操作。
这里的 Canvas 工厂结构中,会判断 useCanvasKit 并结构不同的 Canvas,为什么会有这个逻辑,这里先按下不表,前面会介绍。咱们先依照 Render Pipeline 往下看。
之前提到的 PipeLineOwner 流程完结后,会调用 RenderView 的 compositeFrame 函数进行 Layer 合成。而在 compositeFrame 函数中,咱们能够看到几个十分重要的 Class,那就是 Scene 和 SceneBuilder,Scene 是 Layer 合成结束后的产物,由 SceneBuiler 构建失去。
如图所示,最初它会调用 _window.render 函数,这里的 _window 是 SingletonFlutterWindow,它是一个单例的 RenderView,前面会具体介绍,咱们先看一下 Build Scene 的流程。
这里咱们能够看到 Layer 的局部源码,之前提到 RenderObject 中有一个 ContainerLayer,buildScene 就是调用 ContainerLayer 的 buildScene 函数(如上图的右半局部),随后会调用 Layer 的 addToScene 函数,它和 RenderObject 的 paint 函数相似,也是一个形象函数,须要 Layer 的子类本人去实现,比方 ContainerLayer 的 addToScene 函数就是遍历 Child Tree 来别离调用子 Layer 的 addToScene。
那 addToScene 做了什么呢,它实际上是调用 SceneBuilder 提供的 pushXXX 函数,这些函数的返回值也是 Layer,只不过是 EngineLayer,Layer 是 Framework 中图层的形象,而 EngineLayer 是 Engine 中图层的形象,随后在 Engine 层将这些 EngineLayer 组合到 Scene 中。
2、Flutter Engine
Framework 层曾经介绍得差不多了,接下来咱们来看一下 Engine 层。
简略回顾一下,咱们的 Widget 会经由这样的转换流程:Widget->RenderObject->Layer->EngineLayer->Scene,那么这个 Scene 如何渲染进去呢?
这里咱们看到了之前提到的 SingletonFlutterWindow,它的 render 函数会调用 EnginePlatformDispatcher 的 render 函数,这里咱们又看到了相熟的 useCanvasKit,依据判断将 Scene 强转成了不同的 Scene,那么这个 useCanvasKit 到底示意什么呢,咱们接着往下看。
这个时候咱们必须得引入一个概念,就是 Web Renderer,在 Flutter Web 中有两种渲染模式:一种是基于 HTML 标签的渲染模式,它会将 Flutter 的 Widget 都映射成不同的标签,无奈单纯用标签示意的就会应用 Canvas 进行绘制,有点相似于 ReactNative 的表现形式。
另一种则是基于 CanvasKit 的渲染模式,它会下载 2MB 的 wasm 文件以调用 Skia 渲染引擎,Widget 渲染都是通过该引擎来绘制的。
咱们能够通过命令行参数在 flutter build 或者 run 的时候指定渲染模式,值得一提的是,默认的渲染模式是 auto,在桌面端浏览器上默认是 CanvasKit,而在挪动端 WebView 上默认是 HTML。
首先,咱们来看一下 HTML 渲染模式,以 咱们 Flutter SDK 的 API Example 为例,通过 Elements Tree 能够看到,它的标签层级还是比拟多的,图片中的 <canvas> 标签指向了 “Basic” 的文本,这阐明该模式下文本的渲染应用的是 Canvas,那为什么要应用 Canvas 绘制文本而不应用浏览器默认的文字渲染能力呢?那是因为要抹除平台渲染体现的差别,尤其是文字的换行解决等,Flutter 内置了文字排版的引擎,会基于该引擎进行渲染。此处延长一下,比方输入框组件,在没有获取焦点的状态下,它其实和 Text 是相似的,如果获取了焦点 Flutter 则会增加一个 <input> 标签,而后接管输出的文字信息,当焦点失去的时候再暗藏,这是一个十分奇妙的计划。
接下咱们看一下在 HTML 渲染模式下的一些细节。之前按下不表的 Canvas 在这里就要显示它的真身了,在 HTML 渲染模式下会构建 SurfaceCanvas,能够从右图中看到 List,这就是寄存绘图指令的汇合。
而对于 SceneBuilder,这里的是其子类 SurfaceSceneBuilder,咱们能够先看一下下图中右侧的 PersistedSurface。
它是 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 中。
到这里 HTML 渲染模式就完结了。
上面咱们看一下 CanvasKit 的渲染模式,从 Elements Tree 中咱们能够看到该模式下的层级非常简单,所有的渲染都是在一个 canvas 中进行的,这里用到的 #shadow-root 是 HTML 的一个个性,能够做到款式隔离。
同样,咱们先从 Canvas 动手,这里的是 CanvasKitCanvas,而绘图指令则保留在 CkPictureSnapshot 的 _commands 属性中。
对于 SceneBuilder,CanvasKit 渲染模式下的子类是 LayerSceneBuilder,这里的 Layer 相似于 HTML 渲染模式下的 PersistedSurface,都是派生自 EngineLayer,并且有用一个 ContainerLayer 蕴含所有的 child,也有对应的 PictureLayer 和 PlatformViewLayer。不过不同的是,它有一个 paint 函数,这里的 paint 函数才是真正的操作 GPU 进行绘制的函数。
而 LayerSceneBuilder 的 build 函数生成的 LayerScene 中蕴含一个叫作 LayerTree 的根节点,和 HTML 渲染模式下的 webOnlyRootElement 绝对应。
既然这里提到 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 中。
光栅化阶段由 preroll 和 paint 组成,别离计算绘制边界, 以及遍历 LayerTree 并调用所有 Layer 的 paint 函数,这里的 PaintContext 区别于 Framework 的 PaintingContext,它持有所有的 canvas,以便于不同的 Layer 对其进行 paint 操作。
至此,CanvasKit 渲染模式下的流程也差不多走完了,咱们最初看一下最终是如何显示在 HTML 中的。其实,CanvasKit 渲染模式下最终也应用了 DomRenderer,在 Flutter 的初始化流程中,咱们能够看到,initializeCanvasKit 函数的前半部分是咱们之前提到的引入 Skia 的 wasm 资源和对应的 JavaScript 文件;后半局部则是创立了一个 skiaSceneHost 根节点,这个 Element 就是之前 baseSurface.addToScene 中援用的。
整个渲染原理到这里就介绍完了,当然,整个渲染中还有很多的细节,比方 SurfaceFactory 中除了 baseSurface 还有 backupSurface 能够对绘制进行缓存等,这些点每个开展都能作为一个独自的议题进行探讨。最初贴上一个总结的流程图,大家能够联合前文回顾一下整个流程。
在分享的最初,给大家附上 Flutter RTC SDK 的 GitHub 链接,目前咱们曾经在 dev/flutter 分支上做了 Flutter2 的适配。在 Web 和桌面端上也反对了屏幕共享。大家能够自行体验,如果有任何问题或者倡议,欢送大家反馈,如果应用体验还不错,也欢送大家给咱们的仓库点上 Star。