10分鐘了解Flutter跨平臺(tái)運(yùn)行原理!

導(dǎo)語(yǔ) | 本文將從選型、簡(jiǎn)介和運(yùn)行原理三大部分為你介紹Flutter的相關(guān)概念,希望能站在框架設(shè)計(jì)和實(shí)現(xiàn)原理的高度,帶領(lǐng)大家去理解Flutter區(qū)別其他跨平臺(tái)解決方案的關(guān)鍵所在。
一、為什么選擇Flutter
隨著無(wú)線(xiàn)時(shí)代的來(lái)臨,怎么樣用最標(biāo)準(zhǔn)化的手段能夠讓更多的人開(kāi)發(fā)這個(gè)頁(yè)面、怎么樣能夠提供像H5一樣標(biāo)準(zhǔn)的頁(yè)面,成為大前端時(shí)代開(kāi)發(fā)者們最關(guān)心的事情。

我們把時(shí)間線(xiàn)拉長(zhǎng),來(lái)看看移動(dòng)端跨平臺(tái)技術(shù)經(jīng)過(guò)了一個(gè)怎樣的發(fā)展史:下面主要介紹在這個(gè)發(fā)展過(guò)程中跨平臺(tái)技術(shù)有了哪些進(jìn)步或者做了哪些優(yōu)化。


Ionic/Cordova(Hybrid): 在技術(shù)原理上的核心是,將原生的一些能力通過(guò)JSBridge封裝給Web來(lái)調(diào)用,擴(kuò)充了Web應(yīng)用能力。但是這種方法有兩個(gè)不足,一是依賴(lài)客戶(hù)端,二是在性能和體驗(yàn)上都非常依賴(lài)于Web端。因此,整體上的體驗(yàn)不可預(yù)知。目前這個(gè)技術(shù)還經(jīng)常被應(yīng)用到,例如,當(dāng)前App內(nèi)會(huì)提供白名單域名和可調(diào)用的JSBridge方法,由此來(lái)增強(qiáng)H5與客戶(hù)端交互能力,從而提升App內(nèi)H5的靈活性。
React Native/Weex: 在原來(lái)的Hybrid的JSBridge基礎(chǔ)上進(jìn)行改進(jìn),將JavaScript的界面以及交互轉(zhuǎn)化為Native的控件,從而在體驗(yàn)上和原生界面基本一致。但因?yàn)槭荍IT模式,因此需要頻繁地在JavaScript與Native之間進(jìn)行通信,從而會(huì)有一定的性能損耗影響,導(dǎo)致體驗(yàn)上與原生會(huì)有一些差異。
Flutter: 取長(zhǎng)補(bǔ)短,結(jié)合了之前的一些優(yōu)點(diǎn),解決了與Native之間通信的問(wèn)題,同時(shí)也有了自渲染模式(框架自身實(shí)現(xiàn)了一套UI基礎(chǔ)框架,與原來(lái)的渲染模式基本一致)。從而在體驗(yàn)和性能上相對(duì)之前的兩種框架表現(xiàn)都較好。
選擇Flutter并不是為了代替iOS或者Android,而是做一個(gè)技術(shù)互補(bǔ),比如,F(xiàn)lutter負(fù)責(zé)業(yè)務(wù)功能,而iOS和Android則負(fù)責(zé)部分的底層交互提供服務(wù)給到Flutter應(yīng)用,這里大膽預(yù)測(cè)一下未來(lái)跨端技術(shù)團(tuán)隊(duì)的組成:

二、Flutter簡(jiǎn)介
Flutter是一款移動(dòng)應(yīng)用程序跨平臺(tái)框架,使用一種語(yǔ)言(Dart)編寫(xiě)的同一份代碼可以生成iOS和Android兩個(gè)高性能、高保真的應(yīng)用程序。

Flutter目標(biāo)是使開(kāi)發(fā)人員能夠交付在不同平臺(tái)上都感覺(jué)自然流暢的高性能應(yīng)用程序。兼容滾動(dòng)行為、排版、圖標(biāo)等方面的差異。那么Flutter是如何編譯成原生app的呢?
Flutter不借助原生的渲染能力,而是自己實(shí)現(xiàn)了一套與Android和iOS一樣的渲染原理,從而在性能上與原生平臺(tái)保持基本一致。不過(guò)這里由于目前 Flutter只是一個(gè)UI框架,因此在原生功能方面還是需要依賴(lài)原生平臺(tái),這也是它存在的一些問(wèn)題。

三、Flutter運(yùn)行原理
如前面已提到的那樣,F(xiàn)lutter是重寫(xiě)了一整套包括底層渲染邏輯和上層開(kāi)發(fā)語(yǔ)言的完整解決方案。這樣不僅可以保證視圖渲染在Android和iOS上的高度一致性(即高保真),在代碼執(zhí)行效率和渲染性能上也可以媲美原生App的體驗(yàn)(即高性能)。那Flutter是怎么運(yùn)行的呢?
我們從圖像顯示的基本原理說(shuō)起。
在計(jì)算機(jī)系統(tǒng)中,圖像的顯示需要CPU、GPU和顯示器一起配合完成:CPU負(fù)責(zé)圖像數(shù)據(jù)計(jì)算,GPU負(fù)責(zé)圖像數(shù)據(jù)渲染,而顯示器則負(fù)責(zé)最終圖像顯示。
CPU把計(jì)算好的、需要顯示的內(nèi)容交給GPU,由GPU完成渲染后放入幀緩沖區(qū),隨后視頻控制器根據(jù)垂直同步信號(hào)(VSync)以每秒60次的速度,從幀緩沖區(qū)讀取幀數(shù)據(jù)交由顯示器完成圖像顯示。
操作系統(tǒng)在呈現(xiàn)圖像時(shí)遵循了這種機(jī)制,而Flutter作為跨平臺(tái)開(kāi)發(fā)框架也采用了這種底層方案。下面有一張更為詳盡的示意圖來(lái)解釋Flutter的繪制原理。

可以看到,F(xiàn)lutter關(guān)注如何盡可能快地在兩個(gè)硬件時(shí)鐘的VSync信號(hào)之間計(jì)算并合成視圖數(shù)據(jù),然后通過(guò)Skia交給GPU渲染:UI線(xiàn)程使用Dart來(lái)構(gòu)建視圖結(jié)構(gòu)數(shù)據(jù),這些數(shù)據(jù)會(huì)在GPU線(xiàn)程進(jìn)行圖層合成,隨后交給Skia引擎加工成GPU數(shù)據(jù),而這些數(shù)據(jù)會(huì)通過(guò)OpenGL最終提供給GPU渲染。
在進(jìn)一步學(xué)習(xí)Flutter之前,我們有必要了解下構(gòu)建Flutter的關(guān)鍵技術(shù),即Skia和Dart。
備注:
Skia是一款用C++開(kāi)發(fā)的、性能彪悍的2D圖像繪制引擎,Skia保證了同一套代碼調(diào)用在Android和iOS平臺(tái)上的渲染效果是完全一致的。
Dart是一個(gè)專(zhuān)注于前端(mobile/web)UI(用戶(hù)交互)開(kāi)發(fā)的強(qiáng)類(lèi)型語(yǔ)言。
在了解了Flutter的基本運(yùn)作機(jī)制后,我們?cè)賮?lái)深入了解一下Flutter的實(shí)現(xiàn)原理。
首先,我們來(lái)看一下Flutter的架構(gòu)圖。我希望通過(guò)這張圖以及對(duì)應(yīng)的解讀,你能在開(kāi)始學(xué)習(xí)的時(shí)候就建立起對(duì)Flutter的整體印象。

注:此圖引自Flutter System Overview
Flutter架構(gòu)采用分層設(shè)計(jì),從下到上分為三層,依次為Embedder、Engine和Framework。
Embedder是操作系統(tǒng)適配層,實(shí)現(xiàn)了渲染Surface設(shè)置、線(xiàn)程設(shè)置以及平臺(tái)插件等平臺(tái)相關(guān)特性的適配。從這里我們可以看到,F(xiàn)lutter平臺(tái)相關(guān)特性并不多,這就使得從框架層面保持跨端一致性的成本相對(duì)較低。
Engine層主要包含Skia、Dart和Text,實(shí)現(xiàn)了Flutter的渲染引擎、文字排版、事件處理和Dart運(yùn)行時(shí)等功能。Skia和Text為上層接口提供了調(diào)用底層渲染和排版的能力,Dart則為Flutter提供了運(yùn)行時(shí)調(diào)用Dart和渲染引擎的能力。而Engine層的作用,則是將它們組合起來(lái),從它們生成的數(shù)據(jù)中實(shí)現(xiàn)視圖渲染。
Framework層則是一個(gè)用Dart實(shí)現(xiàn)的UI SDK,包含了動(dòng)畫(huà)、圖形繪制和手勢(shì)識(shí)別等功能。為了在繪制控件等固定樣式的圖形時(shí)提供更直觀、更方便的接口,F(xiàn)lutter還基于這些基礎(chǔ)能力,根據(jù)Material和Cupertino兩種視覺(jué)設(shè)計(jì)風(fēng)格封裝了一套UI組件庫(kù)。我們?cè)陂_(kāi)發(fā)Flutter的時(shí)候,可以直接使用這些組件庫(kù)。
接下來(lái),以界面渲染過(guò)程為例,介紹Flutter是如何工作的。
頁(yè)面中的各界面元素(Widget)以樹(shù)的形式組織,即控件樹(shù)。Flutter通過(guò)控件樹(shù)中的每個(gè)控件創(chuàng)建不同類(lèi)型的渲染對(duì)象,組成渲染對(duì)象樹(shù)。而渲染對(duì)象樹(shù)在Flutter的展示過(guò)程分為三個(gè)階段:布局、繪制、合成和渲染。
(一)布局
Flutter采用深度優(yōu)先機(jī)制遍歷渲染對(duì)象樹(shù),決定渲染對(duì)象樹(shù)中各渲染對(duì)象在屏幕上的位置和尺寸。在布局過(guò)程中,渲染對(duì)象樹(shù)中的每個(gè)渲染對(duì)象都會(huì)接收父對(duì)象的布局約束參數(shù),決定自己的大小,然后父對(duì)象按照控件邏輯決定各個(gè)子對(duì)象的位置,完成布局過(guò)程。

為了防止因子節(jié)點(diǎn)發(fā)生變化而導(dǎo)致整個(gè)控件樹(shù)重新布局,F(xiàn)lutter加入了一個(gè)機(jī)制——布局邊界(Relayout Boundary),可以在某些節(jié)點(diǎn)自動(dòng)或手動(dòng)地設(shè)置布局邊界,當(dāng)邊界內(nèi)的任何對(duì)象發(fā)生重新布局時(shí),不會(huì)影響邊界外的對(duì)象,反之亦然。

(二)繪制
布局完成后,渲染對(duì)象樹(shù)中的每個(gè)節(jié)點(diǎn)都有了明確的尺寸和位置。Flutter會(huì)把所有的渲染對(duì)象繪制到不同的圖層上。與布局過(guò)程一樣,繪制過(guò)程也是深度優(yōu)先遍歷,而且總是先繪制自身,再繪制子節(jié)點(diǎn)。
以下圖為例:節(jié)點(diǎn)1在繪制完自身后,會(huì)再繪制節(jié)點(diǎn)2,然后繪制它的子節(jié)點(diǎn)3、4和5,最后繪制節(jié)點(diǎn)6。

可以看到,由于一些其他原因(比如,視圖手動(dòng)合并)導(dǎo)致2的子節(jié)點(diǎn)5與它的兄弟節(jié)點(diǎn)6處于了同一層,這樣會(huì)導(dǎo)致當(dāng)節(jié)點(diǎn)2需要重繪的時(shí)候,與其無(wú)關(guān)的節(jié)點(diǎn)6也會(huì)被重繪,帶來(lái)性能損耗。
為了解決這一問(wèn)題,F(xiàn)lutter提出了與布局邊界對(duì)應(yīng)的機(jī)制——重繪邊界(Repaint Boundary)。在重繪邊界內(nèi),F(xiàn)lutter會(huì)強(qiáng)制切換新的圖層,這樣就可以避免邊界內(nèi)外的互相影響,避免無(wú)關(guān)內(nèi)容置于同一圖層引起不必要的重繪。

重繪邊界的一個(gè)典型場(chǎng)景是Scrollview。ScrollView滾動(dòng)的時(shí)候需要刷新視圖內(nèi)容,從而觸發(fā)內(nèi)容重繪。而當(dāng)滾動(dòng)內(nèi)容重繪時(shí),一般情況下其他內(nèi)容是不需要重繪的,這時(shí)候重繪邊界就派上用場(chǎng)了。
(三)合成和渲染
終端設(shè)備的頁(yè)面越來(lái)越復(fù)雜,因此Flutter的渲染樹(shù)層級(jí)通常很多,直接交付給渲染引擎進(jìn)行多圖層渲染,可能會(huì)出現(xiàn)大量渲染內(nèi)容的重復(fù)繪制,所以還需要先進(jìn)行一次圖層合成,即將所有的圖層根據(jù)大小、層級(jí)、透明度等規(guī)則計(jì)算出最終的顯示效果,將相同的圖層歸類(lèi)合并,簡(jiǎn)化渲染樹(shù),提高渲染效率。
合并完成后,F(xiàn)lutter會(huì)將幾何圖層數(shù)據(jù)交由Skia引擎加工成二維圖像數(shù)據(jù),最終交由GPU進(jìn)行渲染,完成界面的展示。
四、總結(jié)
咱們從各種業(yè)界主流跨端方案與Flutter的對(duì)比開(kāi)始,到Flutter的簡(jiǎn)要介紹以及Flutter的運(yùn)行機(jī)制,并以界面渲染過(guò)程為例,從布局、繪制、合成和渲染三個(gè)階段講述了Flutter的實(shí)現(xiàn)原理。相信大家對(duì)Flutter已經(jīng)有一個(gè)整體認(rèn)知,趕快一起上手操作起來(lái)吧!
作者簡(jiǎn)介
賓俊文
騰訊IEG直播業(yè)務(wù)部 前端工程師
騰訊IEG直播業(yè)務(wù)部前端工程師。正在為成為極具影響力的工程師而努力!
推薦閱讀
如何在C++20中實(shí)現(xiàn)Coroutine及相關(guān)任務(wù)調(diào)度器?(實(shí)例教學(xué))
拒絕千篇一律,這套Go錯(cuò)誤處理的完整解決方案值得一看!
10個(gè)技巧!實(shí)現(xiàn)Vue.js極致性能優(yōu)化(建議收藏)
為什么WebAssembly不是JavaScript的終結(jié)者,而是它的“助推器”?


