Android开发Android开发经验谈Android技术知识

【淘系技术】超详解析Flutter渲染引擎|业务想创新,不了解底

2020-05-18  本文已影响0人  Android开发架构师

前言

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

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

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

渲染引擎分析

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

image

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

image

具体的运行时步骤:

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

线程模型

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

image

《深入了解 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触发时的调用栈:

image

在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线程的光栅化过程中会用它来做光栅化)

image

渲染指令

当渲染第一帧的时候,会从根节点 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,应该是像下面的样子:

image

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

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

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

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

图片纹理

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

image

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/http://platformviewandroid.cc/CreateResourceContext

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

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

@shell/platform/android/http://androidsurfacegl.cc/SetNativeWindow

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

光栅化与合成

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

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

image

1-4步,在UI线程执行,主要是通过Animator类把LayerTree提交到Pipeline对象的渲染队列,之后通过Shell把pipeline对象提交给GPU线程进行光栅化,不具体展开,代码在animator.cc&pipeline.h
5-6步,在GPU线程执行具体的光栅化操作。这部分主要分为两大块,一块是Surface的管理。一块是如何把Layer Tree里面的渲染指令绘制到之前创建的Surface中。
可以通过下图了解一下Flutter中的Surface,不同类型的Surface,对应不同的底层渲染API。

image

我们以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/http://androidsurfacegl.cc/AndroidSurfaceGL::SetNativeWindow

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

探索

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

小程序渲染引擎

image

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

小程序互动渲染引擎

image

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

总结与思考

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

转载:https://juejin.im/post/5ebb63b96fb9a043674831c9#heading-0

上一篇 下一篇

猜你喜欢

热点阅读