簡介: Flutter從本質上來說仍是一個UI框架,它解決的是一套代碼在多端渲染的問題。在渲染管線的設計上更加精簡,加上自建渲染引擎,相比ReactNative、Weex以及WebView等方案,具備更好的性能體驗。本文將從架構和源碼的角度詳細分析Flutter渲染機制的設計與實現。較長,同窗們可收藏後再看。前端
跨平臺技術因爲其一碼多端的生產力提高而表現出巨大的生命力,從早期的Hybrid App到ReactNative/Weex、小程序/快應用,再到如今的Flutter,跨平臺技術一直在解決效率問題的基礎上最大化的解決性能和體驗問題。這也引出了任何跨平臺技術都會面臨的核心問題:android
效率做爲跨平臺技術的基本功能,你們都能作到。問題是誰能把性能和體驗作得更好,在渲染技術這塊一共有三種方案:git
Flutter因爲其自建渲染引擎,貼近原生的實現方式,得到了優秀的渲染性能。github
Flutter擁有本身的開發工具,開發語言、虛擬機,編譯機制,線程模型和渲染管線,和Android相比,它也能夠看作一個小型的OS了。web
第一次接觸Flutter,能夠看看Flutter的創始人Eric以前的訪談《What is Flutter?》,Eric以前致力於Chromium渲染管線的設計與開發,所以Flutter的渲染與Chromium有必定的類似之處,後面咱們會作下類比。算法
後面咱們會從架構和源碼的角度分析Flutter渲染機制的設計與實現,在此以前也能夠先看看Flutter官方對於渲染機制的分享《How Flutter renders Widgets》。視頻+圖文的方式會更加直觀,能夠有一個大致的理解。macos
從結構上看,Flutter渲染由UI Thread與GPU Thread相互配合完成。canvas
1)UI Thread小程序
對應圖中1-5,執行Dart VM中的Dart代碼(包含應用程序和Flutter框架代碼),主要負責Widget Tree、Element Tree、RenderObject Tree的構建,佈局、以及繪製生成繪製指令,生成Layer Tree(保存繪製指令)等工做。微信小程序
2)GPU Thread
對應圖中6-7,執行Flutter引擎中圖形相關代碼(Skia),這個線程經過與GPU通訊,獲取Layer Tree並執行柵格化以及合成上屏等操做,將Layer Tree顯示在屏幕上。
注:圖層樹(Layer Tree)是Flutter組織繪製指令的方式,相似於Android Rendering裏的View DisplayList,都是組織繪製指令的一種方式。
UI Thread與GPU Thread屬於生產者和消費者的角色。
咱們知道Android上的渲染都是在VSync信號驅動下進行的,Flutter在Android上的渲染也不例外,它會向Android系統註冊並等待VSync信號,等到VSync信號到來之後,調用沿着C++ Engine->Java Engine,到達Dart Framework,開始執行Dart代碼,經歷Layout、Paint等過程,生成一棵Layer Tree,將繪製指令保存在Layer中,接着進行柵格化和合成上屏。
具體說來:
1)向Android系統註冊並等待VSync信號
Flutter引擎啓動時,會向Android系統的Choreographer(管理VSync信號的類)註冊並接收VSync信號的回調。
2)接收到VSync信號,經過C++ Engine向Dart Framework發起渲染調用
當VSync信號產生之後,Flutter註冊的回調被調用,VsyncWaiter::fireCallback() 方法被調用,接着會執行 Animator::BeiginFrame(),最終調用到 Window::BeginFrame() 方法,WIndow實例是鏈接底層Engine和Dart Framework的重要橋樑,基本上與平臺相關的操做都會經過Window實例來鏈接,例如input事件、渲染、無障礙等。
3)Dart Framework開始在UI線程執行渲染邏輯,生成Layer Tree,並將柵格化任務post到GPU線程執行
Window::BeiginFrame() 接着調用,執行到 RenderBinding::drawFrame() 方法,這個方法會去驅動UI界面上的dirty節點(須要重繪的節點)進行從新佈局和繪製,若是渲染過程當中遇到圖片,會先放到Worker Thead去加載和解碼,而後再放到IO Thread生成圖片紋理,因爲IO Thread和GPI Thread共享EGL Context,所以IO Thread生成的圖片紋理能夠被GPU Thread直接訪問。
4)GPU線程接收到Layer Tree,進行柵格化以及合成上屏的工做
Dart Framework繪製完成之後會生成繪製指令保存在Layer Tree中,經過 Animator::RenderFrame() 把Layer Tree提交給GPU Thread,GPU Thread接着執行柵格化和上屏顯示。以後經過 Animator::RequestFrame() 請求接收系統的下一次VSync信號,如此循環往復,驅動UI界面不斷更新。
逐個調用流程比較長,可是核心點沒多少,不用糾結調用鏈,抓住關鍵實現便可,咱們把裏面涉及到的一些主要類用顏色分了個類,對着這個類圖,基本能夠摸清Flutter的脈絡。
綠色:Widget 黃色:Element 紅色:RenderObject
以上即是Flutter渲染的總體流程,會有多個線程配合,多個模塊參與,拋開冗長的調用鏈,咱們針對每一步來具體分析。咱們在分析結構時把Flutter的渲染流程分爲了7大步,Flutter的timeline也能夠清晰地看到這些流程,以下所示:
UI Thread
1)Animate
由 handleBeiginFrame() 方法的transientCallbacks觸發,若是沒有動畫,則該callback爲空;若是有動畫,則會回調 Ticker.tick() 觸發動畫Widget更新下一幀的值。
2)Build
由 BuildOwner.buildScope() 觸發,主要用來構建或者更新三棵樹,Widget Tree、Element Tree和RenderObject Tree。
3)Layout
由 PipelineOwner.flushLayout() 觸發,它會調用 RenderView.performLayout(),遍歷整棵Render Tree,調用每一個節點的 layout(),根據build過程記錄的信息,更新dirty區域RenderObject的排版數據,使得每一個RenderObject最終都能有正確的大小(size)和位置(position,保存在parentData中)。
4)Compositing Bits
由 PipelineOwner.flushCompositingBits() 觸發,更新具備dirty合成位置的渲染對象,此階段每一個渲染對象都會了解其子項是否須要合成,在繪製階段使用此信息選擇如何實現裁剪等視覺效果。
5)Paint
由 PipeOwner.flushPaint() 觸發,它會調用 RenderView.paint()。最終觸發各個節點的 paint(),最終生成一棵Layer Tree,並把繪製指令保存在Layer中。
6)Submit(Compositing)
由 renderView.compositeFrame() 方法觸發,這個地方官方的說法叫Compositing,不過我以爲叫Compositing有歧義,由於它並非在合成,而是把Layer Tree提交給GPU Thread,於是我以爲叫Submit更合適。
GPU Thread
7)Compositing
由 Render.compositeFrame() 觸發,它經過Layer Tree構建一個Scene,傳給Window進行最終的光柵化。
GPU Thread經過Skia向GPU繪製一幀數據,GPU將幀信息保存在FrameBuffer裏,而後根據VSync信號週期性的從FrameBuffer取出幀數據交給顯示器,從而顯示出最終的界面。
Flutter引擎啓動時,向Android系統的Choreographer註冊並接收VSync信號,GPU硬件產生VSync信號之後,系統便會觸發回調,並驅動UI線程進行渲染工做。
觸發方法:由 handleBeiginFrame() 方法的transientCallbacks觸發
Animate在 handleBeiginFrame() 方法裏由transientCallbacks觸發,若是沒有動畫,則該callback爲空;若是有動畫,則會回調 Ticker._tick() 觸發動畫Widget更新下一幀的值。
void handleBeginFrame(Duration rawTimeStamp) { ... try { // TRANSIENT FRAME CALLBACKS Timeline.startSync('Animate', arguments: timelineWhitelistArguments); _schedulerPhase = SchedulerPhase.transientCallbacks; final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; _transientCallbacks = <int, _FrameCallbackEntry>{}; callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { if (!_removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack); }); ... } finally { ... } }
handleBeiginFrame() 處理完成之後,接着調用 handleDrawFrame(),handleDrawFrame() 會觸發如下回調:
這兩個回調都是SchedulerBinding內部的回調隊列,以下所示:
接着會調用 WidgetBinder.drawFrame() 方法,它會先調用會先調用 BuildOwner.buildScope() 觸發樹的更新,而後才進行繪製。
@override void drawFrame() { ... try { if (renderViewElement != null) buildOwner.buildScope(renderViewElement); super.drawFrame(); buildOwner.finalizeTree(); } finally { assert(() { debugBuildingDirtyElements = false; return true; }()); } ... }
接着調用 RenderingBinding.drawFrame() 觸發layout、paingt等流程。
void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); if (sendFramesToEngine) { renderView.compositeFrame(); // this sends the bits to the GPU pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. _firstFrameSent = true; } }
以上即是核心流程代碼,咱們接着來Build的實現。
觸發方法:由 BuildOwner.buildScope() 觸發。
咱們上面說到,handleDrawFrame() 會觸發樹的更新,事實上 BuildOwner.buildScope() 會有兩種調用時機:
也便是說樹的構建和更新都是由 BuildOwner.buildScope() 方法來完成的。它們的差異在於樹構建的時候傳入了一個 element.mount(null, null) 回調。在 buildScope() 過程當中會觸發這個回調。
這個回調會構建三棵樹,爲何會有三棵樹呢,由於Widget只是對UI元素的一個抽象描述,咱們須要先將其inflate成Element,而後生成對應的RenderObject來驅動渲染,以下所示:
觸發方法:由 PipelineOwner.flushLayout() 觸發。
Layout是基於單向數據流來實現的,父節點向子節點傳遞約束(Constraints),子節點向父節點傳遞大小(Size,保存在父節點的parentData變量中)。先深度遍歷RenderObject Tree,而後再遞歸遍歷約束。單向數據流讓佈局流程變得更簡單,性能也更好。
對於RenderObject而言,它只是提供了一套基礎的佈局協議,沒有定義子節點模型、座標系統和具體的佈局協議。它的子類RenderBox則提供了一套笛卡爾座標體系(和Android&iOS同樣),大部分RenderObject類都是直接繼承RenderBox來實現的。RenderBox有幾個不一樣的子類實現,它們各自對應了不一樣的佈局算法。
咱們再來聊聊Layout流程中涉及的兩個概念邊界約束(Constraints)和從新佈局邊界(RelayoutBoundary)。
邊界約束(Constraints):邊界約束是父節點用來限制子節點的大小的一種方式,例如BoxConstraints、SliverConstraints等。
RenderBox提供一套BoxConstraints,如圖所示,它會提供如下限制:
利用這種簡單的盒模型約束,咱們能夠很是靈活的實現不少常見的佈局,例如徹底和父節點同樣的大小,垂直佈局(寬度和父節點同樣大)、水平佈局(高度和父容器同樣大)。
經過Constraints和子節點本身配置的大小信息,就能夠最終算出子節點的大小,接下來就須要計算子節點的位置。子節點的位置是由父節點來決定的。
從新佈局邊界(RelayoutBoundary):爲一個子節點設置從新佈局邊界,這樣當它的大小發生變化時,不會致使父節點從新佈局,這是個標誌位,在標記dirty的markNeedsLayout()方法中會檢查這個標記位來決定是否從新進行佈局。
從新佈局邊界這種機制提高了佈局排版的性能。
經過Layout,咱們瞭解了全部節點的位置和大小,接下來就會去繪製它們。
觸發方法:由 PipelineOwner.flushCompositingBits() 觸發。
在Layout以後,在Paint以前會先執行Compositing Bits,它會檢查RenderObject是否須要重繪,而後更新RenderObject Tree各個節點的needCompositing標誌。若是爲true,則須要重繪。
觸發方法:由 PipeOwner.flushPaint() 觸發。
相關源碼:
咱們知道現代的UI系統都會進行界面的圖層劃分,這樣能夠進行圖層複用,減小繪製量,提高繪製性能,所以Paint(繪製)的核心問題仍是解決繪製命令應該放到哪一個圖層的問題。
Paint的過程也是單向數據流,先向下深度遍歷RenderObject Tree,再遞歸遍歷子節點,遍歷的過程當中會決定每一個子節點的繪製命令應該放在那一層,最終生成Layer Tree。
和Layout同樣,爲了提到繪製性能,繪製階段也引入了從新繪製邊界。
從新繪製邊界(RepaintBoundary):爲一個子節點設置從新繪製邊界,這樣當它須要從新繪製時,不會致使父節點從新繪製,這是個標誌位,在標記dirty的markNeedsPaint()方法中會檢查這個標記位來決定是否從新進行重繪。
事實上這種重繪邊界的機制相對於把圖層分層這個功能開放給了開發者,開發者能夠本身決定本身的頁面那一塊在重繪時不參與重繪(例如滾動容器),以提高總體頁面的性能。從新繪製邊界會改變最終的圖層樹(Layer Tree)結構。
固然這些重繪邊界並不都須要咱們手動放置,大部分Widget組件會自動放置重繪邊界(自動分層)。
設置了RepaintBoundary的就會額外生成一個圖層,其全部的子節點都會被繪製在這個新的圖層上,Flutter中使用圖層來描述一個層次上(一個繪製指令緩衝區)的全部RenderObject,根節點的RenderView會建立Root Layer,而且包含若干個子Layer,每一個Layer又包含多個RenderObject,這些Layer便造成了一個Layer Tree。每一個RenderObject在繪製時,會產生相關的繪製指令和繪製參數,並保存在對應的Layer上。
相關Layer都繼承Layer類,以下所示:
具體能夠參考文章上方的Flutter類圖。
聊完了繪製的基本概念,咱們再來看看繪製的具體流程,上面提到渲染第一幀的時候,會從根節點RenderView開始,逐個遍歷全部子節點進行操做。以下所示:
1)建立Canvas對象
Canvas對象經過PaintCotext獲取,它內部會建立一個PictureLayer,並經過ui.PictureRecorder調用到C++層建立一個Skia的SkPictureRecorder的實例,並經過SkPictureRecorder建立SkCanvas,然後將SkCanvas返回給Dart Framework使用。SkPictureRecorder能夠用來記錄生成的繪製命令。
2)經過Canvas執行繪製
繪製命令會被SkPictureRecorder記錄下來。
3)經過Canvas結束繪製,準備進行柵格化
繪製結束後,會調用 Canvas.stopRecordingIfNeeded() 方法,它會接着去調用C++層的SkPictureRecorder::endRecording()方法生成一個Picture對象並保存在PictureLayer中,Picture對象包含了全部的繪製指令。全部的Layer繪製完成,造成Layer Tree。
繪製完成之後,接着就能夠向GPU Thread提交Layer Tree了。
觸發方法:由 renderView.compositeFrame() 方法觸發。
注:這個地方官方的說法叫Compositing,不過我以爲叫Compositing有歧義,由於它並非在合成,而是把Layer Tree提交給GPU Thread,於是我以爲叫Submit更合適。
void compositeFrame() { Timeline.startSync('Compositing', arguments: timelineArgumentsIndicatingLandmarkEvent); try { final ui.SceneBuilder builder = ui.SceneBuilder(); final ui.Scene scene = layer.buildScene(builder); if (automaticSystemUiAdjustment) _updateSystemChrome(); _window.render(scene); scene.dispose(); assert(() { if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled) debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0); return true; }()); } finally { Timeline.finishSync(); } }
在這個過程當中Dart Framework層的Layer會被轉換爲C++層使用的flow::layer,Flow模塊是一個基於Skia的簡單合成器,運行在GPU線程,並向Skia上傳指令信息。Flutter Engine使用flow緩存Paint階段生成的繪製指令和像素信息。咱們在Paint階段的Layer,它們都與Flow模塊裏的Layer一一對應。
有了包含渲染指令的Layer Tree之後就能夠進行光柵化和合成了。
光柵化是把繪製指令轉換成對應的像素數據,合成是把各圖層柵格化後的數據進行相關的疊加和特性處理。這個流程稱爲Graphics Pipeline。
相關代碼:rasterizer.cc
Flutter採用的是同步光柵化。什麼是同步光柵化?
同步光柵化:
光柵化和合成在一個線程,或者經過線程同步等方式來保證光柵化和合成的的順序。
直接光柵化:直接執行可見圖層的DisplayList中可見區域的繪製指令進行光柵化,在目標Surface的像素緩衝區上生成像素的顏色值。
間接光柵化:爲指定圖層分配額外的像素緩衝區(例如Android提供View.setLayerType容許應用爲指定View提供像素緩衝區,Flutter提供了Relayout Boundary機制來爲特定圖層分配額外緩衝區),該圖層光柵化的過程當中會先寫入自身的像素緩衝區,渲染引擎再將這些圖層的像素緩衝區經過合成輸出到目標Surface的像素緩衝區。
異步分塊光柵化:
圖層會按照必定的規則粉塵一樣大小的圖塊,光柵化以圖塊爲單位進行,每一個光柵化任務執行圖塊區域內的指令,將執行結果寫入分塊的像素緩衝區,光柵化和合成不在一個線程內執行,而且不是同步的。若是合成過程當中,某個分塊沒有完成光柵化,那麼它會保留空白或者繪製一個棋盤格圖形。
Android和Flutter採用同步光柵化策略,以直接光柵化爲主,光柵化和合成同步執行,在合成的過程當中完成光柵化。而Chromium採用異步分塊光柵化測量,圖層會進行分塊,光柵化和合成異步執行。
從文章上方的序列圖能夠看到,光柵化的入口是 Rasterizer::DoDraw() 方法。它會接着調用 ScopedFrame::Raster() 方法,這個方法就是光柵化的核心實現,它主要完成如下工做:
到這裏咱們Flutter總體的渲染實現咱們就分析完了。
Android、Chromium、Flutter都做爲Google家的明星級項目,它們在渲染機制的設計上既有類似又有不一樣,藉着這個機會咱們對它們作個比較。
現代渲染流水線的基本設計:
咱們再分別來看看Android、Chromium和Flutter是怎麼實現的。
Android渲染流水線:
Chromium渲染流水線:
Flutter渲染流水線:
相互比較:
最後的最後,談一談我對跨平臺生態的理解。
跨平臺容器生態至少能夠分爲三個方面:
前端框架生態直接面向的是業務,它應該具有兩個特色:
它應該是擁抱W3C生態的。W3C生態是一個繁榮且充滿活力的生態,它會發展的更久更遠。試圖拋棄W3C生態,自建子集的作法很難走的長遠。這從微信小程序、Flutter都推出for web系列就能看出端倪。
它應該是相對穩定的。不能說咱們每換一套容器,前端的業務就須要從新寫一遍,例如咱們以前作H5容器,後來作小程序容器,由於DSL不通,前端要花大力氣將業務重寫。雖然小程序是一碼多端,可是我認爲這並無解決效率問題,主要存在兩個問題:
在這種狀況下,業務很難實現快速奔跑。因此說無論底層容器怎麼變,前端的框架必定是相對穩定的。而這種穩定性就有賴於容器統一層。
容器統一層是在前端框架和容器層之間的一個層級。它定義了容器提供的基本能力,這些能力就像協議同樣,是相對穩定的。
協議是很是重要的,就像OpenGL協議同樣,有了OpenGL協議,無論底層的渲染方案如何實現,上層的調用是不用變的。對於咱們的業務也是同樣,圍繞着容器統一層,咱們須要沉澱通用的解決方案。
這些東西不能說每搞一套容器,咱們都要大刀闊斧重來一遍,這種作法是有問題的。已經作過的東西,遇到新的技術就推倒重來,只能說明之前定義的方案考慮不周全,沒有考慮沉澱統一和擴展的狀況。
若是咱們自顧自的一遍遍作着功能重複的技術方案,業務能等着咱們嗎。
容器層的迭代核心是爲了在解決效率問題的基礎上最大化的解決性能和體驗問題。
早期的ReactNative模式解決了效率了問題,可是多了一個通訊層(ReactNative是依靠將虛擬DOM的信息傳遞給原生,而後原生根據這些佈局信息構建對應的原生控件樹來實現的原生渲染)存在性能問題,並且這種轉譯的方式須要適配系統版本,帶來更多的兼容性問題。
微信後續又推出了小程序方案,在我看來,小程序方案不像是一個技術方案,它更像是一個商業解決方案,解決了平臺大流量規範管理和分發的問題,給業務方提供通用的技術解決方案,固然小程序底層的渲染方案也是多種多樣的。
後起之秀Flutter解決的痛點是性能能力,它自建了一套GUI系統,底層直接調用Skia圖形庫進行渲染(與Android的機制同樣),進而實現了原生渲染。可是它基於開發效率、性能以及自身生態等因素的考慮最終選擇了Dart,這種作法無疑是直接拋棄了繁榮的前端生態,就跨平臺容器的發展歷史來看,在解決效率與性能的基礎上,最大化的擁抱W3C生態,多是將來最好的方向。Flutter目前也推出了Flutter for Web,從它的思路來看,是先打通Android與iOS,再逐步向Web滲透,咱們期待它的表現。
容器技術是動態向前發展的,咱們今年搞Flutter,明年可能還會搞其餘技術方案。在方案變遷的過程當中,咱們須要保證業務快速平滑的過分,而不是每次大刀闊斧的再來一遍。
隨着手機性能的提高,WebView的性能也愈來愈好,Flutter又爲解決性能問題提供了新的思路,一個基礎設施完善,體驗至上,一碼多端的跨平臺容器生態值得期待。
歡迎加入本地生活終端技術部!
本地生活終端技術部隸屬於阿里本地生活用戶技術部,從事客戶端技術研發工做,主要負責本地生活餓了麼App 和 口碑App 的客戶端架構、基礎中間件、跨平臺技術解決方案,以及帳號、首頁、全局購物車、收銀臺、訂單列表、紅包卡券、直播、短視頻等平臺化核心業務鏈路。目前團隊規模50+人,咱們依託阿里強大的終端技術底盤,以及本地生活的業務土壤,致力於打造最優秀的O2O技術團隊。
招聘本地生活-客戶端開發專家/高級技術專家-杭州/上海/北京,歡迎您的加盟!簡歷發送至 wushi@alibaba-inc.com
附錄
相關平臺
[1]Flutter pub.dev
(https://pub.dev/flutter/packages)
相關文檔
[1]Flutter 官方文檔
(https://flutter.dev/docs/get-started/install/macos)
[2]Flutter for Android developers
(https://flutter.dev/docs/get-started/flutter-for/android-devs)
[3]Flutter Widget Doc
(https://flutter.dev/docs/reference/widgets)
[4]Flutter API Doc(https://api.flutter.dev/)
[5]Dart Doc
(https://dart.dev/guides/language)
相關源碼
[1]Dart Framework
(https://github.com/flutter/flutter/tree/master/packages)
[2]Flutter Engine
(https://github.com/flutter/engine)
相關資源
[1]Flutter Render Pipeline
(https://www.youtube.com/watch?v=UUfXWzp0-DU)
[2]How Flutter renders Widgets
(https://www.youtube.com/watch?v=996ZgFRENMs)
[3]深刻了解Flutter的高性能圖形渲染 video
(https://www.bilibili.com/video/av48772383)
[4]深刻了解Flutter的高性能圖形渲染 ppt
(https://files.flutter-io.cn/events/gdd2018/Deep_Dive_into_Flutter_Graphics_Performance.pdf)