雲栖号資訊:【 點選檢視更多行業資訊】
在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
作者 | 萬紅波(遠湖) 出品 | 阿裡巴巴新零售淘系技術部
前言
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 的運作時序圖:
具體的運作時步驟:
- 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和繪制。
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光栅化并合成。
圖層
在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 開始,逐個周遊所有的子節點進行繪制操作。
我們可以具體看看一個節點如何繪制的:
1.建立 Canvas。繪制時會通過PaintContex擷取的Canvas進行,其内部會去建立一個PictureLayer,并通過ui.PictrureRecorder調用到C++層來建立一個Skia的SkPictureRecorder執行個體,再通過SkPictureRecorder建立SkCanvas,最後把這個SkCanvas傳回給Dart層去使用.
2.通過Canvas 執行具體繪制。Dart 層拿到綁定了底層 SkCanvas 的對象後,用這 Canvas 進行具體的繪制操作,這些繪制指令會被底層的 SkPictureRecorder 記錄下來。
3.結束繪制,準備上屏。繪制完畢時,會調 用Canvas 對象的 stopRecordingIfNeeded 函數,它會最後會去調用 到C++ 的 SkPictureRecorder 的 endRecording 接口來生成一個 Picture 對象,存儲在 PictureLayer 中。
這 Picture 對象對應 Skia 的 SkPicture 對象,存儲這所有的繪制指令。有興趣可以看一下 SkPicture 的官方說明。
所有的 Layer 繪制完成形成 LayerTree,在 renderView.compositeFrame() 中通過 SceneBuilder 把 Dart Layer 映射為 Flutter engine 中的 flow::Layer ,同時也會生成一顆 C++ 的 flow::LayerTree ,存儲在 Scene 對象中,最後通過 Window 的 render 接口送出給 Flutter engine。
在全部繪制操作完成後,在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 紋理。
我們知道,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中實作的:
光栅化完成後,執行frame->Submit()進行合成。這會調用到下面的PresentSurface,來把offscreensurface中的内容轉移到onscreencanvas中,最後通過GLContextPresent()上屏。
GLContextPresent接口代碼如下,實際上是調用的EGL的eglSwapBuffers接口去顯示圖形緩沖區的内容。
上面代碼段中的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作為跨平台渲染的主要形式下,提供了一個更容易定制和優化的方案。
關注「淘系技術」微信公衆号,一個有内容有溫度的技術社群。
作者:阿裡巴巴淘系技術
連結:
https://juejin.im/post/5ebb63b96fb9a043674831c9來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
【雲栖号線上課堂】每天都有産品技術專家分享!
課程位址:
https://yqh.aliyun.com/live立即加入社群,與專家面對面,及時了解課程最新動态!
【雲栖号線上課堂 社群】
https://c.tb.cn/F3.Z8gvnK
原文釋出時間:2020-05-15
本文作者:阿裡巴巴淘系技術
本文來自:“
阿裡巴巴淘系技術”,了解相關資訊可以關注“阿裡巴巴淘系技術”