作者|萬紅波(遠湖)
出品|阿里巴巴新零售淘系技術部
前言
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和繪製。
@ 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);
}
我們可以具體看看一個節點如何繪製的:
- 創建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);
}
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作為跨平臺渲染的主要形式下,提供了一個更容易定製和優化的方案。
關注「淘系技術」微信公眾號,一個有溫度有內容的技術社區~