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

做者 | 万红波(远湖) 出品 | 阿里巴巴新零售淘系技术部css

前言

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

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

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

渲染引擎分析

渲染流水线

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

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

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 的运行时序图:shell

具体的运行时步骤:canvas

  • Flutter 引擎启动时,向系统的 Choreographer 实例注册接收 Vsync 的回调。
  • 平台发出 Vsync 信号后,上一步注册的回调被调用,一系列调用后,执行到 VsyncWaiter::fireCallback。
  • VsyncWaiter::fireCallback实际上会执行Animator类的成员函数BeginFrame。
  • BeginFrame 通过一系列调用执行到 Window 的 BeginFrame,Window 实例是链接底层 Engine 和 Dart framework 的重要桥梁,基本上因此跟平台相关的操做都会由 Window 实例来串联,包括事件,渲染,无障碍等。
  • 经过 Window 的 BeginFrame 调用到 Dart Framework的RenderBinding 类,其有一个方法叫 drawFrame ,这个方法会去驱动 UI 上的 dirty 节点进行重排和绘制,若是遇到图片的显示,会丢到 IO 线程以及去 worker 线程去执行图片加载和解码,解码完成后,再次丢到 IO 线程去生成图片纹理,因为 IO 线程和 GPU 线程是 share GL context 的,因此在 IO 线程生成的图片纹理在 GPU 线程能够直接被 GPU 所处理和显示。
  • 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.dart
void 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);
  }
复制代码
  1. 通 过Canvas 执行具体绘制。Dart 层拿到绑定了底层 SkCanvas 的对象后,用这 Canvas 进行具体的绘制操做,这些绘制命令会被底层的 SkPictureRecorder 记录下来。

  2. 结束绘制,准备上屏。绘制完毕时,会调 用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.dart
void 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.cc
sk_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/platformviewandroid.cc/CreateResourceContext

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

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

@shell/platform/android/androidsurfacegl.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.cc
RasterStatus 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,来把offscreensurface中的内容转移到onscreencanvas中,最后经过GLContextPresent()上屏。

//@shell/GPU/gpu_surface_gl.cc
bool 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.cc
bool 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/androidsurfacegl.cc/AndroidSurfaceGL::SetNativeWindow

总结以上渲染后半段流程,就能够看到LayerTree中的渲染指令被光栅化,并绘制到SkSurface对应的Surface中。这个Surface是由AndroidSurfaceGL建立的一个offscreensurface。再经过PresentSurface操做,把offscreensurface的内容,交换到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做为跨平台渲染的主要形式下,提供了一个更容易定制和优化的方案。

关注「淘系技术」微信公众号,一个有内容有温度的技术社区。

相关文章
相关标签/搜索