深入瞭解Flutter介面開發(強烈推薦)
作者:閒魚技術朝空
概要
本文不是flutter介面開發入門文章,而是一篇深入介紹Flutter framework關於檢視樹的建立與管理機制、佈局、渲染的原理、以及flutter佈局與渲染相關效能最佳化的設計思路的文章。同時介紹在使用flutter開發過程中,遇到的一些坑和相應的解決方案。
Flutter框架簡介
跨平臺應用的框架,沒有使用WebView或者系統平臺自帶的控制元件,使用自身的高效能渲染引擎(Skia)自繪,
介面開發語言使用dart,底層渲染引擎使用C, C++
組合大於繼承,控制元件本身通常由許多小型、單用途的控制元件組成,結合起來產生強大的效果,類的層次結構是扁平的,以最大化可能的組合數量
Rendering Pipeline
本文主要介紹build、layout、paint的三個階段
檢視樹
Widget&Element&RenderObject
flutter檢視樹包含了三種樹,上圖只是介紹了三顆樹的基礎class的對應關係和功能介紹
建立樹
建立widget樹
呼叫runApp(rootWidget),將rootWidget傳給rootElement,做為rootElement的子節點,生成Element樹,由Element樹生成Render樹
Widget:存放渲染內容、檢視佈局資訊,widget的屬性最好都是immutable(如何更新資料呢?檢視後續內容)
Element:存放上下文,透過Element遍歷檢視樹,Element同時持有Widget和RenderObject
RenderObject:根據Widget的佈局屬性進行layout,paint Widget傳人的內容
更新樹
為什麼widget都是immutable?
flutter介面開發是一種響應式程式設計,主張simple is fast,flutter設計的初衷希望資料變更時傳送通知到對應的可變更節點(可能是一個StatefullWidget子節點,也可以是rootWidget),由上到下重新create widget樹進行重新整理,這種思路比較簡單,不用關心資料變更會影響到哪些節點。
widget重新建立,element樹和renderObject樹是否也重新建立?
widget只是一個配置資料結構,建立是非常輕量的,加上flutter團隊對widget的建立/銷燬做了最佳化,不用擔心整個widget樹重新建立所帶來的效能問題,但是renderobject就不一樣了,renderobject涉及到layout、paint等複雜操作,是一個真正渲染的view,整個view 樹重新建立開銷就比較大,所以答案是否定的。
樹的更新規則
找到widget對應的element節點,設定element為dirty,觸發drawframe, drawframe會呼叫element的performRebuild()進行樹重建
widget。build() == null, deactive element。child,刪除子樹,流程結束
element。child。widget == NULL, mount 的新子樹,流程結束
element。child。widget == widget。build() 無需重建,否則進入流程5
Widget。canUpdate(element。child。widget, newWidget) == true,更新child的slot,element。child。update(newWidget)(如果child還有子節點,則遞迴上面的流程進行子樹更新),流程結束,否則轉6
Widget。canUpdate(element。child。widget, newWidget) != true(widget的classtype 或者 key 不相等),deactivew element。child,mount 新子樹
注意事項:
element。child。widget == widget。build(),不會觸發子樹的update,當觸發update的時候,如果沒有生效,要注意widget是否使用舊widget,沒有new widget,導致update流程走到該widget就停止了
子樹的深度變化,會引起子樹重建,如果子樹是一個複雜度很高的樹,可以使用GlobalKey做為子樹widget的key。GlobalKey具有快取功能
如何觸發樹更新
全域性更新:呼叫runApp(rootWidget),一般flutter啟動時呼叫後不再會呼叫
區域性子樹更新, 將該子樹做StatefullWidget的一個子widget,並建立對應的State類例項,透過呼叫state。setState() 觸發該子樹的重新整理
Widget
StatefullWidget vs StatelessWidget
StatelessWidget:無中間狀態變化的widget,需要更新展示內容就得透過重新new,flutter推薦儘量使用StatelessWidget
StatefullWidget:存在中間狀態變化,那麼問題來了,widget不是都immutable的,狀態變化儲存在哪裡?flutter 引入state的類用於存放中間態,透過呼叫state。setState()進行此節點及以下的整個子樹更新
State 生命週期
initState(): state create之後被insert到tree時呼叫的
didUpdateWidget(newWidget):祖先節點rebuild widget時呼叫
deactivate():widget被remove的時候呼叫,一個widget從tree中remove掉,可以在dispose介面被呼叫前,重新instert到一個新tree中
didChangeDependencies():
初始化時,在initState()之後立刻呼叫
當依賴的InheritedWidget rebuild,會觸發此介面被呼叫
build():
After calling [initState]。
After calling [didUpdateWidget]。
After receiving a call to [setState]。
After a dependency of this [State] object changes (e。g。, an[InheritedWidget] referenced by the previous [build] changes)。
After calling [deactivate] and then reinserting the [State] object into the tree at another location。
dispose():Widget徹底銷燬時呼叫
reassemble(): hot reload呼叫
注意事項:
A頁面push一個新的頁面B,A頁面的widget樹中的所有state會依次呼叫deactivate(), didUpdateWidget(newWidget)、build()(這裡懷疑是bug,A頁面push一個新頁面,理論上並沒有將A頁面進行remove操作),當然從功能上,沒有看出來有什麼異常
當ListView中的item滾動出可顯示區域的時候,item會被從樹中remove掉,此item子樹中所有的state都會被dispose,state記錄的資料都會銷燬,item滾動回可顯示區域時,會重新建立全新的state、element、renderobject
使用hot reload功能時,要特別注意state例項是沒有重新建立的,如果該state中存在一下複雜的資源更新需要重新載入才能生效,那麼需要在reassemble()新增處理,不然當你使用hot reload時候可能會出現一些意想不到的結果,例如,要將顯示本地檔案的內容到螢幕上,當你開發過程中,替換了檔案中的內容,但是hot reload沒有觸發重新讀取檔案內容,頁面顯示還是原來的舊內容
資料流轉
從上往下
資料從根往下傳資料,常規做法是一層層往下,當深度變大,資料的傳輸變的困難,flutter提供InheritedWidget用於子節點向祖先節點獲取資料的機制,如下例子:
class FrogColor extends InheritedWidget {
const FrogColor({
Key key,
@required this。color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);
final Color color;
static FrogColor of(BuildContext context) {
return context。inheritFromWidgetOfExactType(FrogColor);
}
@override
bool updateShouldNotify(FrogColor old) => color != old。color;
}
child及其以下的節點可以透過呼叫下面的介面讀取color資料
FrogColor。of(context)。color
說明:BuildContext 就是Element的一個介面類
context。inheritFromWidgetOfExactType(FrogColor)其實是透過context/element往上遍歷樹,查詢到第一個FrogColor的祖先節點,取該節點的widget物件
從下往上
子節點狀態變更,向上上報透過傳送通知的方式
定義通知類,繼承至Notification
父節點使用NotificationListener 進行監聽捕獲通知
子節點有資料變更呼叫下面介面進行資料上報
Notification(data)。dispatch(context)
閒魚flutter的介面框架設計
Layout
Size 計算
parent傳入約束條件,在dramframe的layout階段,child根據自身的渲染內容返回size
問題:在build()階段獲取不到size,很多時候需要提前知道部分widget size來進行佈局,解決方案當widget 在對應renderobject的layout階段之後,傳送一個LayoutChangeNotification,參考SizeChangedLayoutNotifier class,但是SizeChangedLayoutNotifier沒有上報init layout size,可以自己參考這個實現封裝一個Notifier
Offset 計算
renderObject拿到計算好的size,再加上一些佈局屬性(align、paddig)等,計算child相對parent的offset
offset存放在每個child renderObject的BoxParentData中
當parent擁有mutil children時,BoxParentData還用來存children兄弟節點之間的遍歷順序
Relayout boundary
renderObject在layout階段做了Relayout boundary的最佳化,當子樹進行relayout時,滿足下面三種中的一種
parentUsesSize == false
sizedByParent == true
constraints。isTight
那麼該renderObject設定為Relayout boundary,也就是該renderObject的重新layout不觸發parent的layout,一般情況下開發人員不需要關心Relayout boundary,除非是使用CustomMultiChildLayout
Paint
Layer
iOS的每一個UIView都有一個layer,flutter的render object不一定存在layer,一般情況下一個renderObject子樹都渲染在一個layer上,那麼什麼renderObject具有layer,子renderObject怎麼渲染到這個layer?
當一個renderObject的
alwaysNeedsCompositing == true
或者
isRepaintBoundary == true
,renderOject會有對應的compositing layer
子renderObject會對目標layer返回對應的offsetLayer, 目標compositing layer再根據offset合成一個渲染的紋理buffer
Repaint Boundary
類似Relayout boundary,Paint階段也有Repaint Boundary,目的和layout一樣,就是對應子樹的paint不會導致外部的repaint,但是Relayout boundary需要開發人員自己設定,使用RepaintBoundary widget進行設定,ListView在渲染的item預設都是使用了RepaintBoundary,顯而易見ListView的children之間都是相互獨立的。
Flutter建議複雜的image渲染使用RepaintBoundary,image的渲染需要io操作,然後解碼,最後渲染,使用RepaintBoundary可以進行gpu的快取,但是不一定就會快取,engine會判斷這個image是否足夠複雜,畢竟gpu快取還是非常珍貴的,同時RepaintBoundary還會對一些反覆渲染的layer進行快取處理(反覆渲染3次及以上,這個是flutter的影片中提到的)
結語
Flutter還處於Beta階段,有些介面程式設計的介面設計還不夠成熟,相比iOS和安卓生態還很不成熟,需要我們共同的建立,Flutter提供的除錯工具相比一開始接觸的時候,已經完善很多,讓我們給Flutter更多的耐心和包容,期待Flutter越來越完善。
參考資料
https://
github。com/flutter/flut
ter
https://
github。com/flutter/engi
ne
http://
weixin。qq。com/r/Pi4nIyX
EpO3YKWFAb3u6
(二維碼自動識別)