FinOps有望降低企业50%+的云成本! 了解详情
写点什么

超详解析 Flutter 渲染引擎 | 业务想创新,不了解底层原理怎么行?

  • 2020-05-20
  • 本文字数:8073 字

    阅读完需:约 26 分钟

超详解析Flutter渲染引擎 | 业务想创新,不了解底层原理怎么行?

前言

Flutter 作为一个跨平台的应用框架,诞生之后,就被高度关注。它通过自绘 UI ,解决了之前 RN 和 weex 方案难以解决的多端一致性问题。Dart AOT 和精减的渲染管线,相对与 JavaScript 和 webview 的组合,具备更高的性能体验。


目前在集团内也有很多的 BU 在使用和探索。了解底层引擎的工作原理可以帮助我们更深入地结合具体的业务来对引擎进行定制和优化,更好的去创新和支撑业务。在淘宝,我们也基于 Flutter engine 进行了自绘 UI 的渲染引擎的探索。本文先对 Flutter 的底层渲染引擎做一下深入分析和整理,以理清 Flutter 的渲染的机制及思路,之后分享一下我们基于 Flutter 引擎一些探索,供大家参考。


本文的分析主要以 Android 平台为例,IOS 上原理大致类似,相关的参考代码基于 stable/v1.12.13+hotfix.8 。

渲染引擎分析

渲染流水线

整个 Flutter 的 UI 生成以及渲染完成主要分下面几个步骤:



其中 1-6 在收到系统 vsync 信号后,在 UI 线程中执行,主要是涉及在 Dart framework 中 Widget/Element/RenderObject 三颗树的生成以及承载绘制指令的 LayerTree 的创建,7-8 在 GPU 线程中执行,主要涉及光栅化合成上屏。


  • 1-4 跟渲染没有直接关系,主要就是管理 UI 组件生命周期,页面结构以及 Flex layout 等相关实现,本文不作深入分析。

  • 5-8 为渲染相关流程,其中 5-6 在 UI 线程中执行,产物为包含了渲染指令的 Layer tree,在 Dart 层生成,可以认为是整个渲染流程的前半部,属于生产者角色。

  • 7-8 把 dart 层生成的 Layer Tree,通过 window 透传到 Flutter engine 的 C++代码中,通过 flow 模块来实现光栅化并合成输出。可以认为是整个渲染流程的后半部,属于消费者角色。


下图为 Android 平台上渲染一帧 Flutter UI 的运行时序图:



具体的运行时步骤:


  1. flutter 引擎启动时,向系统的 Choreographer 实例注册接收 Vsync 的回调。

  2. 平台发出 Vsync 信号后,上一步注册的回调被调用,一系列调用后,执行到 VsyncWaiter::fireCallback。

  3. VsyncWaiter::fireCallback 实际上会执行 Animator 类的成员函数 BeginFrame。

  4. BeginFrame 经过一系列调用执行到 Window 的 BeginFrame,Window 实例是连接底层 Engine 和 Dart framework 的重要桥梁,基本上所以跟平台相关的操作都会由 Window 实例来串联,包括事件,渲染,无障碍等。

  5. 通过 Window 的 BeginFrame 调用到 Dart Framework 的 RenderBinding 类,其有一个方法叫 drawFrame ,这个方法会去驱动 UI 上的 dirty 节点进行重排和绘制,如果遇到图片的显示,会丢到 IO 线程以及去 worker 线程去执行图片加载和解码,解码完成后,再次丢到 IO 线程去生成图片纹理,由于 IO 线程和 GPU 线程是 share GL context 的,所以在 IO 线程生成的图片纹理在 GPU 线程可以直接被 GPU 所处理和显示。

  6. Dart 层绘制所产生的绘制指令以及相关的渲染属性配置都会存储在 LayerTree 中,通过 Animator::RenderFrame 把 LayerTree 提交到 GPU 线程,GPU 线程拿到 LayerTree 后,进行光栅化并做上屏操作(关于 LayerTree 我们后面会详细讲解)。之后通过 Animator::RequestFrame 请求接收系统下一次的 Vsync 信号,这样又会从第 1 步开始,循环往复,驱动 UI 界面不断的更新。


分析了整个 Flutter 底层引擎总体运行流程,下面会相对详细的分析上述渲染流水线中涉及到的相关概念以及细节知识,大家可以根据自己的情况选择性的阅读。

线程模型

要了解 Flutter 的渲染管线,必须要先了解 Flutter 的线程模型。从渲染引擎的视角来看,Flutter 的四个线程的职责如下:



  • Platform 线程: 负责提供 Native 窗口,作为 GPU 渲染的目标。接受平台的 VSync 信号并发送到 UI 线程,驱动渲染管线运行。

  • UI 线程: 负责 UI 组件管理,维护 3 颗树,Dart VM 管理,UI 渲染指令生成。同时负责把承载渲染指令的 LayerTree 提交给 GPU 线程去光栅化。

  • GPU 线程: 通过 flow 模块完成光栅化,并调用底层渲染 API(opengl/vulkan/meta),合成并输出到屏幕。

  • IO 线程: 包括若干 worker 线程会去请求图片资源并完成图片解码,之后在 IO 线程中生成纹理并上传 GPU ,由于通过和 GPU 线程共享 EGL Context,在 GPU 线程中可以直接使用 IO 线程上传的纹理,通过并行化,提高渲染的性能


后面介绍的概念都会贯穿在这四个线程当中,关于线程模型的更多信息可以参考下面两篇文章:


《深入了解 Flutter 引擎线程模型》


《The Engine architecture》链接见文末

VSync

Flutter 引擎启动时,向系统的 Choreographer 实例注册接收 Vsync 的回调函数,GPU 硬件发出 Vsync 后,系统会触发该回调函数,并驱动 UI 线程进行 layout 和绘制。


@ shell/platform/android/io/flutter/view/VsyncWaiter.java   private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() {        @Override        public void asyncWaitForVsync(long cookie) {            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {                @Override                public void doFrame(long frameTimeNanos) {                    float fps = windowManager.getDefaultDisplay().getRefreshRate();                    long refreshPeriodNanos = (long) (1000000000.0 / fps);                    FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);                }            });        }    };
复制代码


下图为 Vsync 触发时的调用栈:



在 Android 上,Java 层收到系统的 Vsync 的回调后通过 JNI 发给 Flutter engine,之后通过 Animator,Engine 以及 Window 等对象路由调回 dart 层,驱动 dart 层进行 drawFrame 的操作。在 Dart framework 的 RenderingBinding::drawFrame 函数中会触发对所有 dirty 节点的 layout/paint/compositor 相关的操作,之后生成 LayerTree,再交由 Flutter engine 光栅化并合成。


//@rendering/binding.dartvoid drawFrame() {    assert(renderView != null);    pipelineOwner.flushLayout();    pipelineOwner.flushCompositingBits();    pipelineOwner.flushPaint();    renderView.compositeFrame(); // this sends the bits to the GPU    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.  }
复制代码

图层

在 Dart 层进行 drawFrame 对 dirty 节点进行排版后,就会对需要重新绘制的节点进行绘制操作。而我们知道 Flutter 中 widget 是一个 UI 元素的抽象描述,绘制时,需要先将其 inflate 成为 Element,之后生成对应的 RenderObject 来负责驱动渲染。通常来讲,一个页面的所有的 RenderObject 都属于一个图层,Flutter 本身没有图层的概念,这里所说的图层可以粗暴理解成一块内存 buffer,所有属于图层的 RenderObject 都应该被绘制在这个图层对应的 buffer 中去。


如果这个 RenderObject 的 RepaintBoundary 属性为 true 时,就会额外生成一个图层,其所有的子节点都会被绘制在这个新的图层上,最后所有图层有 GPU 来负责合成并上屏。


Flutter 中使用 Layer 的概念来表示一个层次上的所有 RenderObject,Layer 和图层存在 N:1 的对应关系。根节点 RenderView 会创建 root Layer,一般是一个 Transform Layer,并包含多个子 Layer,每个子 Layer 又会包含若干 RenderObject,每个 RenderObject 绘制时,会产生相关的绘制指令和绘制参数,并存储在对应的 Layer 上。


可以参考下面 Layer 的类图,Layer 实际上主要用来组织和存储渲染相关的指令和参数,比如 Transform Layer 用来保存图层变换的矩阵,ClipRectLayer 包含图层的剪切域大小,PlatformViewLayer 包含同层渲染组件的纹理 id,PictureLayer 包含 SkPicture(SkPicture 记录了 SkCanvas 绘制的指令,在 GPU 线程的光栅化过程中会用它来做光栅化)


渲染指令

当渲染第一帧的时候,会从根节点 RenderView 开始,逐个遍历所有的子节点进行绘制操作。


//@rendering/view.dart //绘制入口,从view根节点开始,逐个绘制所有子节点@override  void paint(PaintingContext context, Offset offset) {    if (child != null)      context.paintChild(child, offset);  }
复制代码


我们可以具体看看一个节点如何绘制的:


1. 创建 Canvas。 绘制时会通过 PaintContex 获取的 Canvas 进行,其内部会去创建一个 PictureLayer,并通过 ui.PictrureRecorder 调用到 C++层来创建一个 Skia 的 SkPictureRecorder 实例,再通过 SkPictureRecorder 创建 SkCanvas,最后把这个 SkCanvas 返回给 Dart 层去使用.


//@rendering/object.dart  @override  Canvas get canvas {    if (_canvas == null)      _startRecording();    return _canvas;  }
void _startRecording() { assert(!_isRecording); _currentLayer = PictureLayer(estimatedBounds); _recorder = ui.PictureRecorder(); _canvas = Canvas(_recorder); _containerLayer.append(_currentLayer); }
复制代码


2.通过 Canvas 执行具体绘制。 Dart 层拿到绑定了底层 SkCanvas 的对象后,用这个 Canvas 进行具体的绘制操作,这些绘制命令会被底层的 SkPictureRecorder 记录下来。


3.结束绘制,准备上屏。 绘制完毕时,会调用 Canvas 对象的 stopRecordingIfNeeded 函数,它会最后会去调用到 C++的 SkPictureRecorder 的 endRecording 接口来生成一个 Picture 对象,存储在 PictureLayer 中。


//@rendering/object.dart   void stopRecordingIfNeeded() {    if (!_isRecording)      return;    _currentLayer.picture = _recorder.endRecording();    _currentLayer = null;    _recorder = null;    _canvas = null;  }
复制代码


这个 Picture 对象对应 Skia 的 SkPicture 对象,存储这所有的绘制指令。有兴趣可以看一下 SkPicture 的官方说明。


所有的 Layer 绘制完成形成 LayerTree,在 renderView.compositeFrame()中通过 SceneBuilder 把 Dart Layer 映射为 flutter engine 中的 flow::Layer,同时也会生成一颗 C++的 flow::LayerTree,存储在 Scene 对象中,最后通过 Window 的 render 接口提交给 Flutter engine。


//@rendering/view.dartvoid compositeFrame() {    ...      final ui.SceneBuilder builder = ui.SceneBuilder();      final ui.Scene scene = layer.buildScene(builder);      _window.render(scene);      scene.dispose();  }
复制代码


在全部绘制操作完成后,在 Flutter engine 中就形成了一颗 flow::LayerTree,应该是像下面的样子:



这颗包含了所有绘制信息以及绘制指令的 flow::LayerTree 会通过 window 实例调用到 Animator::Render 后,最后在 Shell::OnAnimatorDraw 中提交给 GPU 线程,并进行光栅化操作,代码可以参考:


@shell/common/animator.cc/Animator::Render


@shell/common/shell.cc/Shell::OnAnimatorDraw


这里提一下 flow 这个模块,flow 是一个基于 skia 的合成器,它可以基于渲染指令来生成像素数据。Flutter 基于 flow 模块来操作 Skia,进行光栅化以及合成。

图片纹理

前面讲线程模型的时候,我们提到过 IO 线程负责图片加载以及解码并且把解码后的数据上传到 GPU 生成纹理,这个纹理在后面光栅化过程中会用到,我们来看一下这部分的内容。



UI 线程加载图片的时候,会在 IO 线程调用 InstantiateImageCodec*函数调用到 C++层来初始化图片解码库,通过 skia 的自带的解码库解码生成 bitmap 数据后,调用 SkImage::MakeCrossContextFromPixmap 来生成可以在多个线程共享的 SkImage,在 IO 线程中用它来生成 GPU 纹理。


//@flutter/lib/ui/painting/codec.ccsk_sp<SkImage> MultiFrameCodec::GetNextFrameImage(    fml::WeakPtr<GrContext> resourceContext) {  ...  // 如果resourceContext不为空,就会去创建一个SkImage,  // 并且这个SkImage是在resouceContext中的,  if (resourceContext) {    SkPixmap pixmap(bitmap.info(), bitmap.pixelRef()->pixels(),                    bitmap.pixelRef()->rowBytes());    // This indicates that we do not want a "linear blending" decode.    sk_sp<SkColorSpace> dstColorSpace = nullptr;    return SkImage::MakeCrossContextFromPixmap(resourceContext.get(), pixmap,                                               false, dstColorSpace.get());  } else {    // Defer decoding until time of draw later on the GPU thread. Can happen    // when GL operations are currently forbidden such as in the background    // on iOS.    return SkImage::MakeFromBitmap(bitmap);  }}
复制代码


我们知道,OpenGL 的环境是线程不安全的,在一个线程生成的图片纹理,在另外一个线程里面是不能直接使用的。但由于上传纹理操作比较耗时,都放在 GPU 线程操作,会减低渲染性能。目前 OpenGL 中可以通过 share context 来支持这种多线程纹理上传的,所以目前 flutter 中是由 IO 线程做纹理上传,GPU 线程负责使用纹理。


基本的操作就是在 GPU 线程创建一个 EGLContextA,之后把 EGLContextA 传给 IO 线程,IO 线程在通过 EGLCreateContext 在创建 EGLContextB 的时候,把 EGLContextA 作为 shareContext 的参数,这样 EGLContextA 和 EGLContextB 就可以共享纹理数据了。


具体相关的代码不一一列举了,可以参考:


@shell/platform/android/platform_view_android.cc/CreateResourceContext


@shell/platform/android/android_surface_gl.cc/ResourceContextMakeCurrent


@shell/platform/android/android_surface_gl.cc/AndroidSurfaceGL


@shell/platform/android/android_surface_gl.cc/SetNativeWindow


关于图片加载相关流程,可以参考这篇文章:TODO

光栅化与合成

把绘制指令转化为像素数据的过程称为光栅化,把各图层光栅化后的数据进行相关的叠加与特效相关的处理成为合成这是渲染后半段的主要工作。


前面也提到过,生成 LayerTree 后,会通过 Window 的 Render 接口把它提交到 GPU 线程去执行光栅化操作,大体流程如下:



1-4 步,在 UI 线程执行,主要是通过 Animator 类把 LayerTree 提交到 Pipeline 对象的渲染队列,之后通过 Shell 把 pipeline 对象提交给 GPU 线程进行光栅化,不具体展开,代码在 animator.cc&pipeline.h


5-6 步,在 GPU 线程执行具体的光栅化操作。这部分主要分为两大块,一块是 Surface 的管理。一块是如何把 Layer Tree 里面的渲染指令绘制到之前创建的 Surface 中。


可以通过下图了解一下 Flutter 中的 Surface,不同类型的 Surface,对应不同的底层渲染 API。



我们以 GPUSurfaceGL 为例,在 Flutter 中,GPUSurfaceGL 是对 Skia GrContext 的一个管理和封装,而 GrContext 是 Skia 用来管理 GPU 绘制的一个上下文,最终都是借助它来操作 OpenGL 的 API 进行相关的上屏操作。在引擎初始化时,当 FlutterViewAndroid 创建后,就会创建 GPUSurfaceGL,在其构造函数中会同步创建 Skia 的 GrContext。



光栅化主要是在函数 Rasterizer::DrawToSurface 中实现的:


//@shell/rasterizer.ccRasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {  FML_DCHECK(surface_);  ...   if (compositor_frame) {    //1.执行光栅化    RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);    if (raster_status == RasterStatus::kFailed) {      return raster_status;    }    //2.合成    frame->Submit();    if (external_view_embedder != nullptr) {      external_view_embedder->SubmitFrame(surface_->GetContext());    }    //3.上屏    FireNextFrameCallbackIfPresent();
if (surface_->GetContext()) { surface_->GetContext()->performDeferredCleanup(kSkiaCleanupExpiration); }
return raster_status; }
return RasterStatus::kFailed;}
复制代码


光栅化完成后,执行 frame->Submit()进行合成。这会调用到下面的 PresentSurface,来把 offscreen_surface 中的内容转移到 onscreen_canvas 中,最后通过 GLContextPresent()上屏。


//@shell/GPU/gpu_surface_gl.ccbool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) {...  if (offscreen_surface_ != nullptr) {    SkPaint paint;    SkCanvas* onscreen_canvas = onscreen_surface_->getCanvas();    onscreen_canvas->clear(SK_ColorTRANSPARENT);    // 1.转移offscreen surface的内容到onscreen canvas中    onscreen_canvas->drawImage(offscreen_surface_->makeImageSnapshot(), 0, 0,                               &paint);  }  {    //2. flush 所有绘制命令    onscreen_surface_->getCanvas()->flush();  }   //3 上屏  if (!delegate_->GLContextPresent()) {    return false;  }  ...  return true;}
复制代码


GLContextPresent 接口代码如下,实际上是调用的 EGL 的 eglSwapBuffers 接口去显示图形缓冲区的内容。


//@shell/platform/android/android_surface_gl.ccbool AndroidSurfaceGL::GLContextPresent() {  FML_DCHECK(onscreen_context_ && onscreen_context_->IsValid());  return onscreen_context_->SwapBuffers();}
复制代码


上面代码段中的 onscreen_context 是 Flutter 引擎初始化的时候,通过 setNativeWindow 获得。主要是把一个 Android 的 SurfaceView 组件对应的 ANativeWindow 指针传给 EGL,EGL 根据这个窗口,调用 eglCreateWindowSurface 和显示系统建立关联,之后通过这个窗口把渲染内容显示到屏幕上。


代码可以参考:


@shell/platform/android/android_surface_gl.cc/AndroidSurfaceGL::SetNativeWindow


总结以上渲染后半段流程,就可以看到 LayerTree 中的渲染指令被光栅化,并绘制到 SkSurface 对应的 Surface 中。这个 Surface 是由 AndroidSurfaceGL 创建的一个 offscreen_surface。再通过 PresentSurface 操作,把 offscreen_surface 的内容,交换到 onscreen_surface 中去,之后调用 eglSwapSurfaces 上屏,结束一帧的渲染。

探索

深入了解了 Flutter 引擎的渲染机制后,基于业务的诉求,我们也做了一些相关的探索,这里简单分享一下。

小程序渲染引擎


基于 Flutter engine,我们去除了原生的 dart 引擎,引入 js 引擎,用 C++重写了 Flutter Framework 中的 rendering,painting 以及 widget 的核心逻辑,继续向上封装基础组件,实现 cssom 以及 C++版的响应式框架,对外提供统一的 JS Binding API,再向上对接小程序的 DSL,供小程序业务方使用。对于性能要求比较高的小程序,可以选择使用这条链路进行渲染,线下我们跑通了星巴克小程序的 UI 渲染,并具备了很好的性能体验。

小程序互动渲染引擎


受限于小程序 worker/render 的架构,互动业务中频繁的绘制操作需要经过序列化/反序列化并把消息从 worker 发送到 render 去执行渲染命令。基于 flutter engine,我们提供了一套独立的 2d 渲染引擎,引入 canvas 的渲染管线,提供标准的 canvas API 供业务直接在 worker 线程中使用,缩短渲染链路,提高性能。目前已经支持了相关的互动业务在线上运行,性能和稳定性表现很好。

总结与思考

本文着重分析了 flutter engine 的渲染流水线及其相关概念并简单分享了我们的一些探索。熟悉和了解渲染引擎的工作原来可以帮助我们在 Android 和 IOS 双端快速去构建一个差异化高效的渲染链路。这在目前双端主要以 web 作为跨平台渲染的主要形式下,提供了一个更容易定制和优化的方案。


参考链接



本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650406624&idx=1&sn=160171f46f25fd26cc34a5e37298c4cc&chksm=839536f8b4e2bfee5cf77c94b2594567afc8d99dc625d3c96beef794242e152d4fdd2494083f&scene=27#wechat_redirect


2020-05-20 14:045862

评论

发布
暂无评论
发现更多内容

并发编程中,你加的锁未必安全

华为云开发者联盟

线程 高并发 并发 线程安全

大厂算法面试之leetcode精讲9.位运算

全栈潇晨

算法 LeetCode

Perforce用户文章转载:每个游戏从业者都应该学学P4

龙智—DevSecOps解决方案

版本控制 游戏开发 版本管理 perforce 游戏厂商

移动计算云分布式数据缓存服务,实现快速可靠的跨区域多活复制

华为云开发者联盟

可用性 云数据缓存 跨区域多活 无冲突复制数据类型CRDT

大批量更新数据mysql批量更新的四种方法

大数据技术指南

11月日更

一文讲透一致性哈希的原理和实现

万俊峰Kevin

微服务 高并发 哈希算法 go-zero Go 语言

react源码解析3.react源码架构

buchila11

源码 React React Hooks react源码

dart系列之:时间你慢点走,我要在dart中抓住你

程序那些事

flutter 架构 dart 程序那些事 11月日更

PackML从会到不会——状态机(1)

陈的错题集

标准化 PackML

如何使用 Java 代码给图片增加倒影效果

Jerry Wang

Java API 图片处理 11月日更 Java图片

Rainbond通过插件整合SkyWalking,实现APM即插即用

北京好雨科技有限公司

Kubernetes 云原生 全链路追踪

16张图解锁Spring的整体脉络

4ye

Java spring 程序员 后端 签约计划第二季

react源码解析4.源码目录结构和调试

buchila11

React React Hooks

Java开发中常用的消息队列工具 ActiveMQ

编程江湖

Activemq Java 开发

博文推荐|深入解析 Apache Pulsar 中的事务

Apache Pulsar

大数据 架构 分布式 云原生 Apache Pulsar

爱奇艺TFServing负载均衡问题研究及改进实践

爱奇艺技术产品团队

Python量化数据仓库搭建系列2:Python操作数据库

恒生LIGHT云社区

Python 量化

数仓开发详细剖析

五分钟学大数据

11月日更

服务API版本控制设计与实践

vivo互联网技术

API 服务器端开发 客户端开发 迭代

Google I/O 2021 What's new in Android Machine Learning

CatTalk

机器学习 tensorflow android Google

测试不趁早,“持续测试”搞不好

SoFlu软件机器人

DevOps 敏捷开发 自动化测试

万字讲解WiFi为何物

华为云开发者联盟

wifi 物联网 无线通信 传输 无线

Linux学习方法《Linux一学就会》Centos8软件包的管理与安装

侠盗安全

Linux linux运维 运维工程师 云计算架构师

直播预告|数以智用——大数据应用探索与实践

智联卓聘

大数据 数据管理 线上沙龙

如何在P4中管理Unreal Engine 代码

龙智—DevSecOps解决方案

版本控制 游戏开发 版本管理 游戏引擎 虚幻引擎

java开发之DOS命令学习及运行环境配置安装

@零度

java开发学习 DOS命令学习

百度商业大规模高性能全息日志检索技术揭秘

百度Geek说

软件架构

前端开发之JavaScript优化

@零度

JavaScript 大前端

大厂算法面试之leetcode精讲10.递归&分治

全栈潇晨

LeetCode 算法面试

内存数据库的分布式架构提升之道

鲸品堂

数据库

Elasticsearch云生态下的开源共生之路

大咖说

云计算 elasticsearch 开源

  • 需要帮助,请添加网站小助手,进入 InfoQ 技术交流群
超详解析Flutter渲染引擎 | 业务想创新,不了解底层原理怎么行?_硬件_万红波(远湖)_InfoQ精选文章