简介:Flutter 做为一个跨平台的应用框架,诞生以后,就被高度关注。它经过自绘 UI ,解决了以前 RN 和 weex 方案难以解决的多端一致性问题。Dart AOT 和精减的渲染管线,相对与 JavaScript 和 webview 的组合,具有更高的性能体验。 本文的分析主要以 Android 平台为例,IOS 上原理大体相似,相关的参考代码基于 stable/v1.12.13+hotfix.8 。css
做者|万红波(远湖)
出品|阿里巴巴新零售淘系技术部java
Flutter 做为一个跨平台的应用框架,诞生以后,就被高度关注。它经过自绘 UI ,解决了以前 RN 和 weex 方案难以解决的多端一致性问题。Dart AOT 和精减的渲染管线,相对与 JavaScript 和 webview 的组合,具有更高的性能体验。android
目前在集团内也有不少的 BU 在使用和探索。了解底层引擎的工做原理能够帮助咱们更深刻地结合具体的业务来对引擎进行定制和优化,更好的去创新和支撑业务。在淘宝,咱们也基于 Flutter engine 进行了自绘UI的渲染引擎的探索。本文先对 Flutter 的底层渲染引擎作一下深刻分析和整理,以理清 Flutter 的渲染的机制及思路,以后分享一下咱们基于Flutter引擎一些探索,供你们参考。web
本文的分析主要以 Android 平台为例,IOS 上原理大体相似,相关的参考代码基于 stable/v1.12.13+hotfix.8 。shell
▐ 渲染流水线canvas
整个 Flutter 的 UI 生成以及渲染完成主要分下面几个步骤:小程序
其中 1-6 在收到系统 vsync 信号后,在 UI 线程中执行,主要是涉及在 Dart framework 中 Widget/Element/RenderObject 三颗树的生成以及承载绘制指令的 LayerTree 的建立,7-8 在 GPU 线程中执行,主要涉及光栅化合成上屏。安全
下图为 Android 平台上渲染一帧 Flutter UI 的运行时序图:cookie
具体的运行时步骤:weex
分析了整个 Flutter 底层引擎整体运行流程,下面会相对详细的分析上述渲染流水线中涉及到的相关概念以及细节知识,你们能够根据本身的状况选择性的阅读。
▐ 线程模型
要了解 Flutter 的渲染管线,必需要先了解 Flutter 的线程模型。从渲染引擎的视角来看,Flutter 的四个线程的职责以下:
后面介绍的概念都会贯穿在这四个线程当中,关于线程模型的更多信息能够参考下面两篇文章:
《深刻了解 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光栅化并合成。
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); }
咱们能够具体看看一个节点如何绘制的:
//@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.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/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.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,来把offscreen_surface中的内容转移到onscreen_canvas中,最后经过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/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做为跨平台渲染的主要形式下,提供了一个更容易定制和优化的方案。