【淘系技術】超詳解析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做爲跨平臺渲染的主要形式下,提供了一個更容易定製和優化的方案。

關注「淘系技術」微信公衆號,一個有內容有溫度的技術社區。

相關文章
相關標籤/搜索