Flutter之美
作者丨搜狐焦點-鮑立志
來源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)
本文字數(shù):4326字
預計閱讀時間:13分鐘
?本文旨在盡量避開具體的代碼細節(jié),從思想上去介紹flutter的各種技術(shù)實現(xiàn),讓已經(jīng)在從事flutter開發(fā)的同學有更多的收獲,同時對flutter感興趣的觀望者也能更好的了解這門技術(shù)
一、flutter能給我們帶來什么?
跨越多個平臺的能力
由于不依賴平臺,使用獨立渲染的方式,可以在多個平臺上高效運行:
android、ios:
所有ui部分的開發(fā)都可以完全獨立進行,同時具備與原生平臺互調(diào)用的能力,通過制作插件的方式,可以完美使用現(xiàn)有兩個移動平臺的生態(tài)資源,大部分輪子不需要再重新制作
windows、mac、linux:
由于native的相似性,移動平臺的插件大部分都可以兼容pc平臺,把移動端代碼遷移到桌面端非常容易,一套app的代碼可以在桌面端運行,這成本真的太低了,太吸引人了
web:
要將dart代碼生成js代碼,打包體積比較大,生態(tài)資源也不如js,用到native特性的插件都不支持web,代碼共用困難,目前很少使用
Fuchsia:
Google自研的物聯(lián)網(wǎng)操作系統(tǒng),基于Zircon微內(nèi)核(非linux),未來2年內(nèi)大概率面世。它將flutter作為框架層,相信未來大面積使用這個操作系統(tǒng)的時候,flutter技術(shù)也會迎來一個新的高潮
開發(fā)的時間成本大大降低
強大的跨平臺能力,已經(jīng)大大節(jié)約了我們開發(fā)的人力時間成本,但flutter的強大遠不止如此,下面我來一一介紹
熱重載:
同rn一樣,擁有jit即時編譯的能力,可以在調(diào)試時無需重新運行,修改代碼后可以立馬同步到界面。想想android開發(fā)中,特別大型的項目,編譯超過3分鐘的不少見,每天幾十次的編譯運行能節(jié)省出大概1小時以上的時間,可以早早打開下班啦
代碼一鍵定位:
先來看看android開發(fā)者的一個痛點,在不熟悉代碼的情況下,如果需要改個ui的話,可能需要以下幾個步驟:
首先要定位是哪個activity,通過命令或者layout inspector工具。
知道是哪個 ?activity之后,打開對應的xml布局配置文件,如果design里不能明顯看出來,就要通過id的命名去找
如果這個組件就是在activity里,那就可以直接找到id對應的組件了。如果這個組件不在activity里,比如在RecyclerView里,那就需要找到它對應的viewholder,繼續(xù)在布局里找。
如果是代碼生成的組件,那么就抱歉了,只能寄希望于代碼量不是很大了...
好吧,真的是一言難盡
再來看看flutter的一鍵定位功能:
點擊開啟視圖定位模式

點擊想要定位的ui組件

成功定位到該組件的代碼位置

是不是又可以提前下班的時間了?。。?/p>
高效的dart語言:
我個人認為dart語言可能是移動端開發(fā)最好的語言了,下面就說說它好在哪
擁有java一樣的強類型特性,是一款類型安全的語言,并且支持dynamic類型,需要的話,可以像js一樣靈活
支持函數(shù)式編程,代碼更為簡潔
mixin方式實現(xiàn)多繼承,比內(nèi)部類更為優(yōu)雅
Flutter2.0開始支持空安全,不需要再到處判空了
基本上借鑒了java、js、kotlin的優(yōu)點,開發(fā)效率會得到很大的提升
好了,現(xiàn)在我們知道了flutter相較于傳統(tǒng)移動開發(fā)的強大之處,后面我們將詳細介紹flutter的設計原理和機制,包括整體架構(gòu)、線程模型、渲染過程等
二、Flutter框架全景

Dart 框架層(Framework)
上層框架,主要包括 dart 側(cè) Widget 管理、繪制、動畫、手勢等接口
C++ 引擎層(Engine)
虛擬機、線程模型、與平臺的通信、繪制流程、系統(tǒng)事件、文字布局、幀渲染管線等
平臺相關(guān)的嵌入層(Embeder)
渲染圖層、平臺線程和事件循環(huán)管理,Native Plugin 等
三、flutter的界面渲染過程
視圖樹的構(gòu)建流程
flutter中的視圖樹借鑒了react的思想,也和android中的mvvm類似,核心思想就是ui和數(shù)據(jù)綁定,數(shù)據(jù)變化之后重新構(gòu)建ui,來達到ui更新的目的。實現(xiàn)這種方式構(gòu)建ui有兩個必要條件,一是要比較兩次view樹變化的區(qū)域,這個只需要完成對應的diff算法就很容易解決。二是需要頻繁創(chuàng)建、銷毀view樹的配置對象,需要在內(nèi)存管理方面做到高效,后面會講到dart虛擬機是如何應對這種頻繁gc的情況。下面讓我們來看看具體的ui構(gòu)建過程
1. 聲明式創(chuàng)建widget樹:
widget樹就是一份簡單的、輕量級的配置信息,并不是真正的視圖組件節(jié)點。它是不可變的,不可修改的,為什么呢?想想我們在android開發(fā)里,每一個ui組件可以在xml布局文件里創(chuàng)建,又可以在代碼中隨意修改,這樣造成的結(jié)果就是如果你在設備上看到一個組件被修改了,那么在xml文件里不一定能找到修改的出處,同樣在代碼里也要去找很久,因為view的引用可以被隨意傳遞,這實在太可怕了,這太不可控了,太不利于維護了。
Flutter怎么做的?
Flutter使用聲明式構(gòu)建ui,完全解決了這個痛點,widget不能修改,只能重新聲明去更新它,這也是它為什么是輕量級的,重建的代價不大。如果被修改了,從聲明處開始尋找,結(jié)合一鍵定位,可以快速找到修改它的出處。
2. 生成element樹
這就是真正的視圖組件節(jié)點了,它和widget樹一一對應,會比較新的widget樹和原來widget樹的變化,只更新變化的節(jié)點。
3. 根據(jù)element生成的RenderObject樹進行渲染。
需要新創(chuàng)建的節(jié)點它會將配置信息解析出RenderObject并持有它,用來處理具體的布局和繪制。需要更新的節(jié)點則只需要修改RenderObject,不需要重新創(chuàng)建。由此實現(xiàn)了widget樹變化后進行最小范圍的處理,性能由此得到提升。RenderObject不和上面兩棵樹一一對應,它只是具體要渲染的節(jié)點,比如StatelessWidget只是組合了其他widget,不需要為他單獨生成一個RenderObject

布局與繪制
1. 布局
先回想一下android中的布局:測量一般會進行2次,第一次進行模糊測量,第二次根據(jù)子view大小確定具體的測量值,然后布局。一旦其中一個view有了變化,又需要重新布局。它的缺點顯而易見:多次測量,view變化后影響較大。來看看flutter是如何優(yōu)化這兩個不足之處的吧

每個節(jié)點都有一個布局約束,即maxWidth,minWidth,maxHeight,minHeight,這個約束是根據(jù)父節(jié)點的約束和自己本身的約束得到的。這樣就只需要一次后續(xù)遍歷便可以確定每一個view的大小和位置。如此,就實現(xiàn)了單次布局

使用RelayoutBoundary進行布局邊界限制,邊界內(nèi)的組件發(fā)生變化,邊界外不重新布局
2. 繪制
為了避免沒必要的重繪,每一個RenderObject都有一個isRepaintBoundary屬性,即繪制邊界,通過這個邊界來進行繪制區(qū)域的隔斷

重繪標記
如圖,節(jié)點4被標記為需要重新繪制,它的isRepaintBoundary=false,會向上查找,直到找到節(jié)點2的isRepaintBoundary=true,將節(jié)點2加入到重繪列表中,即真正進行重繪的節(jié)點
繪制
如圖,節(jié)點2存在于重繪制列表中,會進行一個先序遍歷2->3->4->5,依次繪制。為什么到5就結(jié)束了?因為5也是一個繪制邊界,由此確定出最小的繪制區(qū)域。節(jié)點1和節(jié)點6都不進行重新繪制
為什么不是所有節(jié)點都使用RepaintBoundary?
RepaintBoundary強制使用新的圖層進行繪制,可以避免無關(guān)自己的重復繪制。如果圖層過多,也會使得渲染性能下降,所以只需要將無需重復繪制的部分使用RepaintBoundary就能做到最大的性能優(yōu)化
RepaintBoundary應用
最典型的應用就是CustomScrollView,在滑動過程中,會不斷的重繪子組件,如果我們的子組件中有較為復雜的繪制邏輯,就
3. 合成和渲染
終端設備的頁面越來越復雜,因此flutter的渲染樹層級通常很多,直接交付給渲染引擎進行多圖層渲染,可能會出現(xiàn)大量渲染內(nèi)容的重復繪制,所以還需要先進行依次圖層的合成,即將所有的圖層根據(jù)大小、層級、透明度等規(guī)則計算出最終的顯示效果,將相同的圖層歸類合并,簡化渲染樹,提高渲染效率。
四、Dart虛擬機原理
單線程模型
所謂單線程模型,就是將任務放在隊列中輪詢執(zhí)行,以此來實現(xiàn)異步任務,dart線程中有兩個任務隊列:microtask queue優(yōu)先級更高,如果它里面有未處理的事件,會優(yōu)先從這里取出事件處理。event queue,一般的異步任務都是放在這里的。

為什么要用單線程模型呢?
前端開發(fā)大部分異步任務都是為了等待,比如網(wǎng)絡請求的等待,數(shù)據(jù)庫、文件數(shù)據(jù)讀取的等待,IO密集型的任務是不消耗cpu的,為此使用多線程反而浪費資源,單線程模型更為合理
單線程模型里不需要多線程共享內(nèi)存,就不必擔心同步死鎖這些問題,開發(fā)效率得到提升
無鎖的內(nèi)存分配是可以實現(xiàn)內(nèi)存的線性分配的,不用查找可用內(nèi)存空間,內(nèi)存分配的效率得到提升
異步任務理解
如何使用異步任務
使用Future傳入一個方法就會把一個任務放在Event Queue中了,當這個異步任務執(zhí)行完畢后,會將Future.then()里的函數(shù)添加到MicroTask Queue中,由此可以更優(yōu)先去處理異步任務的結(jié)果
void?main()?{
??Future(()?{
????print("future1");
??});
??Future?future2?=?Future(()?{});
??Future(()?{
????print("future3-1");
??}).then((value)?{
????print("future3-2");
????scheduleMicrotask(()?{
??????print("future3-microtask");
????});
??}).then((value)?{
????print("future3-3");
??});
??Future(()?{
????print("future4-1");
??}).then((value)?{
????Future(()?{
??????print("future4-2");
????});
??}).then((value)?{
????print("future4-3");
??});
??future2.then((value)?{
????print("future2-1");
??});
??scheduleMicrotask(()?{
????print("microtask?1");
??});
??print("main");
}
輸出結(jié)果為:
main
microtask?1
future1
future2-1
future3-1
future3-2
future3-3
future3-microtask
future4-1
future4-3
future4-2
Event Loop 優(yōu)先執(zhí)行 main 方法同步任務,再執(zhí)行微任務,最后執(zhí)行 Event Queue 的異步任務。所以 main先執(zhí)行
同理微任務 microtask 1 執(zhí)行
其次,Event Queue FIFO,future1 被執(zhí)行
future2 內(nèi)部為空,所以 then 里的內(nèi)容被加到微任務隊列中去,微任務優(yōu)先級最高,所以 future2-1 被執(zhí)行
其次,future3-1 被執(zhí)行。由于存在2個 then,先執(zhí)行第一個 then 中的 future3-2,然后遇到微任務,所以 future3-microtask 被添加到微任務隊列中去,等待下一次 Event Loop 到來時觸發(fā)。接著執(zhí)行第二個 then 中的 future3-3。隨著下一次 Event Loop 到來,future3-microtask 被執(zhí)行
其次,future4-1 被執(zhí)行。隨后的第一個 then 中的任務又是被 Future 包裝成一個異步任務,被添加到 Event Queue 中,第二個 then 中的內(nèi)容也被添加到 Event Queue 中。
接著,執(zhí)行 future4-3。本次事件循環(huán)結(jié)束
等下一輪事件循環(huán)到來,打印隊列中的 future4-2
async和await
用async修飾函數(shù),表示異步方法,但如果async方法中沒有出現(xiàn)await,它仍然是一個同步方法:

執(zhí)行順序為:a
? ? ? ? ? ? ?b
? ???????????c
?????????????main
只有當遇到await時,才會將之后的內(nèi)容打包成一個Future放入event queue中

執(zhí)行順序為:a
? ? ? ? ? ? ?main
? ? ? ? ? ? ?b
? ? ? ? ? ? ?c
Future的其他用法
Future.catcheError() ?用于處理異常
Future.whenComplete() ?無論是否發(fā)生異常都會進行回調(diào)
Future.wait() 接收一個Future數(shù)組,等待所有異步任務完成
Completer 它持有一個future,可以通過Completer.complete()來自己控制future完成,相當于一個一次性使用的listener注冊
多線程
單線程模型不是為了替代多線程而存在的,只是為了在大量io密集型場景下進行高效開發(fā)所設計的,如果我們遇到了算法密集型任務,繼續(xù)使用單線程,那么就會導致我們的ui線程卡頓了,所以在算法密集型任務里使用多線程來最大化cpu的利用率是必不可少的。
dart里的線程叫做Isolate,意思為隔離,和他的名字類似,兩個Isolate之間是不能共享內(nèi)存的,是獨立的,更像是進程的感覺。前面講到單線程模型的好處,不必操心同步死鎖問題,不用查找可用內(nèi)存進行無鎖的內(nèi)存分配,所以即便使用多線程,也就不能夠進行共享內(nèi)存了。兩個線程之間的通信可以通過管道實現(xiàn)
start()?async?{
??ReceivePort?receivePort?=?ReceivePort();?//?創(chuàng)建管道
??Isolate?isolate?=?await?Isolate.spawn(coding,?receivePort.sendPort);?//?創(chuàng)建?Isolate,并傳遞發(fā)送管道作為參數(shù)
??//?監(jiān)聽消息
??receivePort.listen((message)?{
??});
}
內(nèi)存分配和垃圾回收
內(nèi)存分配
DartVM的內(nèi)存分配策略非常簡單,創(chuàng)建對象時只需要在現(xiàn)有堆上移動指針,內(nèi)存增長始終是線性的,省去了查找可用內(nèi)存段的過程 每個Isolate之間是無法共享內(nèi)存的,所以這種分配策略可以讓Dart實現(xiàn)無鎖的快速分配。
垃圾回收
Dart的垃圾回收也采用了多生代算法,新生代在回收內(nèi)存時采用了 “半空間” 算法,觸發(fā)垃圾回收時Dart會將當前半空間中的“活躍”對象拷貝到備用空間,然后整體釋放當前空間的所有內(nèi)存 整個過程中Dart只需要操作少量的“活躍”對象,大量的沒有引用的“死亡”對象則被忽略,這種多生代無鎖垃圾回收器,專門為UI框架中常見的大量Widgets對象創(chuàng)建和銷毀優(yōu)化,非常適合Flutter框架中大量Widget重建的場景.
五、理解Runner
什么是Runner?
通過全景圖中可以看到,dart VM中的isolate其實也是被Embedder所分配和管理的,所以Root Isolate也只是flutter運行時的其中一個線程,還有其他的一些線程由engine進行管理,稱為Runner

UI runner:
負責處理root isolate 中的代碼執(zhí)行、界面布局、繪制、生成 layer tree 等
GPU runner:
負責將 layer tree 信息轉(zhuǎn)為 GPU 指令,配置繪制所需資源
IO runner:
配合 GPU runner,主要負責讀取圖片、解碼,上傳到 GPU 等耗時操作
Platform runner:
負責處理 Engine 與外部的所有交互 ,同時處理平臺相關(guān)的所有事件,即平臺主線程。平臺主線程與flutter 的ui線程相互獨立,平臺線程卡頓不會影響flutter ui界面。
渲染過程中各個Runner之間的邏輯
Root Isolate 需要創(chuàng)建或重新渲染一個frame時,通知engine
engine通過Platform Runner 監(jiān)聽來自GPU Runner的 vsync信號
Platform Runner 收到vsync信號 通知engine,engine通知Root Isolate進行Widget Tree的build以及布局、繪制、合成Layer Tree
Root Isolate 將Layer Tree 交給engine,engine發(fā)送給GPU Runner,GPU Runner配置好資源,將Layer Tree生成GPU指令交給GPU進行最后的渲染
六.總結(jié)
相信你已經(jīng)看到了flutter的強大之處,跨平臺能力、高效的開發(fā)體驗、先進的前端框架設計思想??赡芪ㄒ坏牟蛔憔褪撬鼘嵲谑翘贻p了,但是也能看到在短短不到三年的時間里它所取得的成就,這足夠讓人興奮。
現(xiàn)在已經(jīng)到了flutter 2.5版本,每隔幾個月就會有一次大的版本更新,每次的升級都會帶來不同的驚喜,Google爸爸確實是對它寄予了厚望,可能也是想盡快為Fuchsia把路鋪的更平吧。
Flutter的未來值得期待!
引用
1.https://juejin.cn/post/6844903901641048077?
2.https://juejin.cn/post/6974363413942108197
3.https://juejin.cn/post/6890951845729009671
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?面試題?資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
