超详解析Flutter渲染引擎|业务想创新,不了解底层原理怎么行?
簡(jiǎn)介:?Flutter 作為一個(gè)跨平臺(tái)的應(yīng)用框架,誕生之后,就被高度關(guān)注。它通過(guò)自繪 UI ,解決了之前 RN 和 weex 方案難以解決的多端一致性問(wèn)題。Dart AOT 和精減的渲染管線,相對(duì)與 JavaScript 和 webview 的組合,具備更高的性能體驗(yàn)。 本文的分析主要以 Android 平臺(tái)為例,IOS 上原理大致類似,相關(guān)的參考代碼基于 stable/v1.12.13+hotfix.8 。
作者|萬(wàn)紅波(遠(yuǎn)湖)
出品|阿里巴巴新零售淘系技術(shù)部
前言
Flutter 作為一個(gè)跨平臺(tái)的應(yīng)用框架,誕生之后,就被高度關(guān)注。它通過(guò)自繪 UI ,解決了之前 RN 和 weex 方案難以解決的多端一致性問(wèn)題。Dart AOT 和精減的渲染管線,相對(duì)與 JavaScript 和 webview 的組合,具備更高的性能體驗(yàn)。
目前在集團(tuán)內(nèi)也有很多的 BU 在使用和探索。了解底層引擎的工作原理可以幫助我們更深入地結(jié)合具體的業(yè)務(wù)來(lái)對(duì)引擎進(jìn)行定制和優(yōu)化,更好的去創(chuàng)新和支撐業(yè)務(wù)。在淘寶,我們也基于 Flutter engine 進(jìn)行了自繪UI的渲染引擎的探索。本文先對(duì) Flutter 的底層渲染引擎做一下深入分析和整理,以理清 Flutter 的渲染的機(jī)制及思路,之后分享一下我們基于Flutter引擎一些探索,供大家參考。
本文的分析主要以 Android 平臺(tái)為例,IOS 上原理大致類似,相關(guān)的參考代碼基于 stable/v1.12.13+hotfix.8 。
渲染引擎分析
? 渲染流水線
整個(gè) Flutter 的 UI 生成以及渲染完成主要分下面幾個(gè)步驟:
其中 1-6 在收到系統(tǒng) vsync 信號(hào)后,在 UI 線程中執(zhí)行,主要是涉及在 Dart framework 中 Widget/Element/RenderObject 三顆樹(shù)的生成以及承載繪制指令的 LayerTree 的創(chuàng)建,7-8 在 GPU 線程中執(zhí)行,主要涉及光柵化合成上屏。
- 1-4跟渲染沒(méi)有直接關(guān)系,主要就是管理UI組件生命周期,頁(yè)面結(jié)構(gòu)以及Flex layout等相關(guān)實(shí)現(xiàn),本文不作深入分析。
- 5-8為渲染相關(guān)流程,其中5-6在UI線程中執(zhí)行,產(chǎn)物為包含了渲染指令的Layer tree,在Dart層生成,可以認(rèn)為是整個(gè)渲染流程的前半部,屬于生產(chǎn)者角色。
- 7-8把dart層生成的Layer Tree,通過(guò)window透?jìng)鞯紽lutter engine的C++代碼中,通過(guò)flow模塊來(lái)實(shí)現(xiàn)光柵化并合成輸出。可以認(rèn)為是整個(gè)渲染流程的后半部,屬于消費(fèi)者角色。
下圖為 Android 平臺(tái)上渲染一幀 Flutter UI 的運(yùn)行時(shí)序圖:
具體的運(yùn)行時(shí)步驟:
分析了整個(gè) Flutter 底層引擎總體運(yùn)行流程,下面會(huì)相對(duì)詳細(xì)的分析上述渲染流水線中涉及到的相關(guān)概念以及細(xì)節(jié)知識(shí),大家可以根據(jù)自己的情況選擇性的閱讀。
? 線程模型
要了解 Flutter 的渲染管線,必須要先了解 Flutter 的線程模型。從渲染引擎的視角來(lái)看,Flutter 的四個(gè)線程的職責(zé)如下:
- Platform 線程:負(fù)責(zé)提供Native窗口,作為GPU渲染的目標(biāo)。接受平臺(tái)的VSync信號(hào)并發(fā)送到UI線程,驅(qū)動(dòng)渲染管線運(yùn)行。
- UI 線程:負(fù)責(zé)UI組件管理,維護(hù)3顆樹(shù),Dart VM管理,UI渲染指令生成。同時(shí)負(fù)責(zé)把承載渲染指令的LayerTree提交給GPU線程去光柵化。
- GPU線程:通過(guò)flow模塊完成光柵化,并調(diào)用底層渲染API(opengl/vulkan/meta),合成并輸出到屏幕。
- IO 線程:包括若干worker線程會(huì)去請(qǐng)求圖片資源并完成圖片解碼,之后在 IO 線程中生成紋理并上傳 GPU ,由于通過(guò)和 GPU 線程共享 EGL Context,在 GPU 線程中可以直接使用 IO 線程上傳的紋理,通過(guò)并行化,提高渲染的性能
后面介紹的概念都會(huì)貫穿在這四個(gè)線程當(dāng)中,關(guān)于線程模型的更多信息可以參考下面兩篇文章:
《深入了解 Flutter 引擎線程模型》
《The Engine architecture》
? VSync
Flutter引擎啟動(dòng)時(shí),向系統(tǒng)的Choreographer實(shí)例注冊(cè)接收Vsync的回調(diào)函數(shù),GPU硬件發(fā)出Vsync后,系統(tǒng)會(huì)觸發(fā)該回調(diào)函數(shù),并驅(qū)動(dòng)UI線程進(jìn)行l(wèi)ayout和繪制。
@ shell/platform/android/io/flutter/view/VsyncWaiter.java private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() {@Overridepublic void asyncWaitForVsync(long cookie) {Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {@Overridepublic void doFrame(long frameTimeNanos) {float fps = windowManager.getDefaultDisplay().getRefreshRate();long refreshPeriodNanos = (long) (1000000000.0 / fps);FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);}});}};下圖為Vsync觸發(fā)時(shí)的調(diào)用棧:
在Android上,Java層收到系統(tǒng)的Vsync的回調(diào)后通過(guò)JNI發(fā)給Flutter engine,之后通過(guò)Animator,Engine以及Window等對(duì)象路由調(diào)回dart層,驅(qū)動(dòng)dart層進(jìn)行drawFrame的操作。在Dart framework的RenderingBinding::drawFrame函數(shù)中會(huì)觸發(fā)對(duì)所有dirty節(jié)點(diǎn)的layout/paint/compositor相關(guān)的操作,之后生成LayerTree,再交由Flutter engine光柵化并合成。
void drawFrame() {assert(renderView != null);pipelineOwner.flushLayout();pipelineOwner.flushCompositingBits();pipelineOwner.flushPaint();renderView.compositeFrame(); // this sends the bits to the GPUpipelineOwner.flushSemantics(); // this also sends the semantics to the OS.}? 圖層
在Dart層進(jìn)行drawFrame對(duì)dirty節(jié)點(diǎn)進(jìn)行排版后,就會(huì)對(duì)需要重新繪制的節(jié)點(diǎn)進(jìn)行繪制操作。而我們知道Flutter中widget是一個(gè)UI元素的抽象描述,繪制時(shí),需要先將其inflate成為Element,之后生成對(duì)應(yīng)的RenderObject來(lái)負(fù)責(zé)驅(qū)動(dòng)渲染。通常來(lái)講,一個(gè)頁(yè)面的所有的RenderObject都屬于一個(gè)圖層,Flutter本身沒(méi)有圖層的概念,這里所說(shuō)的圖層可以粗暴理解成一塊內(nèi)存buffer,所有屬于圖層的RenderObject都應(yīng)該被繪制在這個(gè)圖層對(duì)應(yīng)的buffer中去。
如果這個(gè)RenderObject的RepaintBoundary屬性為true時(shí),就會(huì)額外生成一個(gè)圖層,其所有的子節(jié)點(diǎn)都會(huì)被繪制在這個(gè)新的圖層上,最后所有圖層有GPU來(lái)負(fù)責(zé)合成并上屏。
Flutter中使用Layer的概念來(lái)表示一個(gè)層次上的所有RenderObject,Layer和圖層存在N:1的對(duì)應(yīng)關(guān)系。根節(jié)點(diǎn)RenderView會(huì)創(chuàng)建root Layer,一般是一個(gè)Transform Layer,并包含多個(gè)子Layer,每個(gè)子Layer又會(huì)包含若干RenderObject,每個(gè)RenderObject繪制時(shí),會(huì)產(chǎn)生相關(guān)的繪制指令和繪制參數(shù),并存儲(chǔ)在對(duì)應(yīng)的Layer上。
可以參考下面Layer的類圖,Layer實(shí)際上主要用來(lái)組織和存儲(chǔ)渲染相關(guān)的指令和參數(shù),比如Transform Layer用來(lái)保存圖層變換的矩陣,ClipRectLayer包含圖層的剪切域大小,PlatformViewLayer包含同層渲染組件的紋理id,PictureLayer包含SkPicture(SkPicture記錄了SkCanvas繪制的指令,在GPU線程的光柵化過(guò)程中會(huì)用它來(lái)做光柵化)
? 渲染指令
當(dāng)渲染第一幀的時(shí)候,會(huì)從根節(jié)點(diǎn)RenderView開(kāi)始,逐個(gè)遍歷所有的子節(jié)點(diǎn)進(jìn)行繪制操作。
//@rendering/view.dart //繪制入口,從view根節(jié)點(diǎn)開(kāi)始,逐個(gè)繪制所有子節(jié)點(diǎn) @overridevoid paint(PaintingContext context, Offset offset) {if (child != null)context.paintChild(child, offset);}我們可以具體看看一個(gè)節(jié)點(diǎn)如何繪制的:
2.通過(guò)Canvas執(zhí)行具體繪制。Dart層拿到綁定了底層SkCanvas的對(duì)象后,用這個(gè)Canvas進(jìn)行具體的繪制操作,這些繪制命令會(huì)被底層的SkPictureRecorder記錄下來(lái)。
3.結(jié)束繪制,準(zhǔn)備上屏。繪制完畢時(shí),會(huì)調(diào)用Canvas對(duì)象的stopRecordingIfNeeded函數(shù),它會(huì)最后會(huì)去調(diào)用到C++的SkPictureRecorder的endRecording接口來(lái)生成一個(gè)Picture對(duì)象,存儲(chǔ)在PictureLayer中。
//@rendering/object.dart void stopRecordingIfNeeded() {if (!_isRecording)return;_currentLayer.picture = _recorder.endRecording();_currentLayer = null;_recorder = null;_canvas = null;}這個(gè)Picture對(duì)象對(duì)應(yīng)Skia的SkPicture對(duì)象,存儲(chǔ)這所有的繪制指令。有興趣可以看一下SkPicture的官方說(shuō)明。
所有的Layer繪制完成形成LayerTree,在renderView.compositeFrame()中通過(guò)SceneBuilder把Dart Layer映射為flutter engine中的flow::Layer,同時(shí)也會(huì)生成一顆C++的flow::LayerTree,存儲(chǔ)在Scene對(duì)象中,最后通過(guò)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,應(yīng)該是像下面的樣子:
這顆包含了所有繪制信息以及繪制指令的flow::LayerTree會(huì)通過(guò)window實(shí)例調(diào)用到Animator::Render后,最后在Shell::OnAnimatorDraw中提交給GPU線程,并進(jìn)行光柵化操作,代碼可以參考:
@shell/common/animator.cc/Animator::Render
@shell/common/shell.cc/Shell::OnAnimatorDraw
這里提一下flow這個(gè)模塊,flow是一個(gè)基于skia的合成器,它可以基于渲染指令來(lái)生成像素?cái)?shù)據(jù)。Flutter基于flow模塊來(lái)操作Skia,進(jìn)行光柵化以及合成。
? 圖片紋理
前面講線程模型的時(shí)候,我們提到過(guò)IO線程負(fù)責(zé)圖片加載以及解碼并且把解碼后的數(shù)據(jù)上傳到GPU生成紋理,這個(gè)紋理在后面光柵化過(guò)程中會(huì)用到,我們來(lái)看一下這部分的內(nèi)容。
UI線程加載圖片的時(shí)候,會(huì)在IO線程調(diào)用InstantiateImageCodec*函數(shù)調(diào)用到C++層來(lái)初始化圖片解碼庫(kù),通過(guò)skia的自帶的解碼庫(kù)解碼生成bitmap數(shù)據(jù)后,調(diào)用SkImage::MakeCrossContextFromPixmap來(lái)生成可以在多個(gè)線程共享的SkImage,在IO線程中用它來(lái)生成GPU紋理。
//@flutter/lib/ui/painting/codec.cc sk_sp<SkImage> MultiFrameCodec::GetNextFrameImage(fml::WeakPtr<GrContext> resourceContext) {...// 如果resourceContext不為空,就會(huì)去創(chuàng)建一個(gè)SkImage,// 并且這個(gè)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的環(huán)境是線程不安全的,在一個(gè)線程生成的圖片紋理,在另外一個(gè)線程里面是不能直接使用的。但由于上傳紋理操作比較耗時(shí),都放在GPU線程操作,會(huì)減低渲染性能。目前OpenGL中可以通過(guò)share context來(lái)支持這種多線程紋理上傳的,所以目前flutter中是由IO線程做紋理上傳,GPU線程負(fù)責(zé)使用紋理。
基本的操作就是在GPU線程創(chuàng)建一個(gè)EGLContextA,之后把EGLContextA傳給IO線程,IO線程在通過(guò)EGLCreateContext在創(chuàng)建EGLContextB的時(shí)候,把EGLContextA作為shareContext的參數(shù),這樣EGLContextA和EGLContextB就可以共享紋理數(shù)據(jù)了。
具體相關(guān)的代碼不一一列舉了,可以參考:
@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
關(guān)于圖片加載相關(guān)流程,可以參考這篇文章:TODO
? 光柵化與合成
把繪制指令轉(zhuǎn)化為像素?cái)?shù)據(jù)的過(guò)程稱為光柵化,把各圖層光柵化后的數(shù)據(jù)進(jìn)行相關(guān)的疊加與特效相關(guān)的處理成為合成這是渲染后半段的主要工作。
前面也提到過(guò),生成LayerTree后,會(huì)通過(guò)Window的Render接口把它提交到GPU線程去執(zhí)行光柵化操作,大體流程如下:
1-4步,在UI線程執(zhí)行,主要是通過(guò)Animator類把LayerTree提交到Pipeline對(duì)象的渲染隊(duì)列,之后通過(guò)Shell把pipeline對(duì)象提交給GPU線程進(jìn)行光柵化,不具體展開(kāi),代碼在animator.cc&pipeline.h
5-6步,在GPU線程執(zhí)行具體的光柵化操作。這部分主要分為兩大塊,一塊是Surface的管理。一塊是如何把Layer Tree里面的渲染指令繪制到之前創(chuàng)建的Surface中。
可以通過(guò)下圖了解一下Flutter中的Surface,不同類型的Surface,對(duì)應(yīng)不同的底層渲染API。
我們以GPUSurfaceGL為例,在Flutter中,GPUSurfaceGL是對(duì)Skia GrContext的一個(gè)管理和封裝,而GrContext是Skia用來(lái)管理GPU繪制的一個(gè)上下文,最終都是借助它來(lái)操作OpenGL的API進(jìn)行相關(guān)的上屏操作。在引擎初始化時(shí),當(dāng)FlutterViewAndroid創(chuàng)建后,就會(huì)創(chuàng)建GPUSurfaceGL,在其構(gòu)造函數(shù)中會(huì)同步創(chuàng)建Skia的GrContext。
光柵化主要是在函數(shù)Rasterizer::DrawToSurface中實(shí)現(xiàn)的:
//@shell/rasterizer.cc RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {FML_DCHECK(surface_);... if (compositor_frame) {//1.執(zhí)行光柵化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; }光柵化完成后,執(zhí)行frame->Submit()進(jìn)行合成。這會(huì)調(diào)用到下面的PresentSurface,來(lái)把offscreen_surface中的內(nèi)容轉(zhuǎn)移到onscreen_canvas中,最后通過(guò)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.轉(zhuǎn)移offscreen surface的內(nèi)容到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接口代碼如下,實(shí)際上是調(diào)用的EGL的eglSwapBuffers接口去顯示圖形緩沖區(qū)的內(nèi)容。
//@shell/platform/android/android_surface_gl.cc
bool AndroidSurfaceGL::GLContextPresent() {
FML_DCHECK(onscreen_context_ && onscreen_context_->IsValid());
return onscreen_context_->SwapBuffers();
}
上面代碼段中的onscreen_context是Flutter引擎初始化的時(shí)候,通過(guò)setNativeWindow獲得。主要是把一個(gè)Android的SurfaceView組件對(duì)應(yīng)的ANativeWindow指針傳給EGL,EGL根據(jù)這個(gè)窗口,調(diào)用eglCreateWindowSurface和顯示系統(tǒng)建立關(guān)聯(lián),之后通過(guò)這個(gè)窗口把渲染內(nèi)容顯示到屏幕上。
代碼可以參考:
@shell/platform/android/android_surface_gl.cc/AndroidSurfaceGL::SetNativeWindow
總結(jié)以上渲染后半段流程,就可以看到LayerTree中的渲染指令被光柵化,并繪制到SkSurface對(duì)應(yīng)的Surface中。這個(gè)Surface是由AndroidSurfaceGL創(chuàng)建的一個(gè)offscreen_surface。再通過(guò)PresentSurface操作,把offscreen_surface的內(nèi)容,交換到onscreen_surface中去,之后調(diào)用eglSwapSurfaces上屏,結(jié)束一幀的渲染。
探索
深入了解了Flutter引擎的渲染機(jī)制后,基于業(yè)務(wù)的訴求,我們也做了一些相關(guān)的探索,這里簡(jiǎn)單分享一下。
? 小程序渲染引擎
基于Flutter engine,我們?nèi)コ嗽膁art引擎,引入js引擎,用C++重寫了Flutter Framework中的rendering,painting以及widget的核心邏輯,繼續(xù)向上封裝基礎(chǔ)組件,實(shí)現(xiàn)cssom以及C++版的響應(yīng)式框架,對(duì)外提供統(tǒng)一的JS Binding API,再向上對(duì)接小程序的DSL,供小程序業(yè)務(wù)方使用。對(duì)于性能要求比較高的小程序,可以選擇使用這條鏈路進(jìn)行渲染,線下我們跑通了星巴克小程序的UI渲染,并具備了很好的性能體驗(yàn)。
? 小程序互動(dòng)渲染引擎
受限于小程序worker/render的架構(gòu),互動(dòng)業(yè)務(wù)中頻繁的繪制操作需要經(jīng)過(guò)序列化/反序列化并把消息從worker發(fā)送到render去執(zhí)行渲染命令。基于flutter engine,我們提供了一套獨(dú)立的2d渲染引擎,引入canvas的渲染管線,提供標(biāo)準(zhǔn)的canvas API供業(yè)務(wù)直接在worker線程中使用,縮短渲染鏈路,提高性能。目前已經(jīng)支持了相關(guān)的互動(dòng)業(yè)務(wù)在線上運(yùn)行,性能和穩(wěn)定性表現(xiàn)很好。
總結(jié)與思考
本文著重分析了flutter engine的渲染流水線及其相關(guān)概念并簡(jiǎn)單分享了我們的一些探索。熟悉和了解渲染引擎的工作原來(lái)可以幫助我們?cè)贏ndroid和IOS雙端快速去構(gòu)建一個(gè)差異化高效的渲染鏈路。這在目前雙端主要以web作為跨平臺(tái)渲染的主要形式下,提供了一個(gè)更容易定制和優(yōu)化的方案。
總結(jié)
以上是生活随笔為你收集整理的超详解析Flutter渲染引擎|业务想创新,不了解底层原理怎么行?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 小伙用智能 AI 修复100 年前京城的
- 下一篇: 在家办公这些天整理的Kafka知识点大全