深刻了解Flutter界面開發

閒魚技術-朝空編程

概要

本文不是flutter界面開發入門文章,而是一篇深刻介紹Flutter framework關於視圖樹的建立與管理機制、佈局、渲染的原理、以及flutter佈局與渲染相關性能優化的設計思路的文章。同時介紹在使用flutter開發過程當中,遇到的一些坑和相應的解決方案。緩存

Flutter框架簡介

  1. 跨平臺應用的框架,沒有使用WebView或者系統平臺自帶的控件,使用自身的高性能渲染引擎(Skia)自繪,
  2. 界面開發語言使用dart,底層渲染引擎使用C, C++
  3. 組合大於繼承,控件自己一般由許多小型、單用途的控件組成,結合起來產生強大的效果,類的層次結構是扁平的,以最大化可能的組合數量

Rendering Pipeline

本文主要介紹build、layout、paint的三個階段性能優化

視圖樹

Widget&Element&RenderObject

flutter視圖樹包含了三種樹,上圖只是介紹了三顆樹的基礎class的對應關係和功能介紹數據結構

建立樹

  1. 建立widget樹
  2. 調用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 樹從新建立開銷就比較大,因此答案是否認的。less

樹的更新規則

  1. 找到widget對應的element節點,設置element爲dirty,觸發drawframe, drawframe會調用element的performRebuild()進行樹重建
  2. widget.build() == null, deactive element.child,刪除子樹,流程結束
  3. element.child.widget == NULL, mount 的新子樹,流程結束
  4. element.child.widget == widget.build() 無需重建,不然進入流程5
  5. Widget.canUpdate(element.child.widget, newWidget) == true,更新child的slot,element.child.update(newWidget)(若是child還有子節點,則遞歸上面的流程進行子樹更新),流程結束,不然轉6
  6. Widget.canUpdate(element.child.widget, newWidget) != true(widget的classtype 或者 key 不相等),deactivew element.child,mount 新子樹

注意事項:ide

  1. element.child.widget == widget.build(),不會觸發子樹的update,當觸發update的時候,若是沒有生效,要注意widget是否使用舊widget,沒有new widget,致使update流程走到該widget就中止了
  2. 子樹的深度變化,會引發子樹重建,若是子樹是一個複雜度很高的樹,可使用GlobalKey作爲子樹widget的key。GlobalKey具備緩存功能

如何觸發樹更新

  1. 全局更新:調用runApp(rootWidget),通常flutter啓動時調用後再也不會調用
  2. 局部子樹更新, 將該子樹作StatefullWidget的一個子widget,並建立對應的State類實例,經過調用state.setState() 觸發該子樹的刷新

Widget

StatefullWidget vs StatelessWidget

  1. StatelessWidget:無中間狀態變化的widget,須要更新展現內容就得經過從新new,flutter推薦儘可能使用StatelessWidget
  2. StatefullWidget:存在中間狀態變化,那麼問題來了,widget不是都immutable的,狀態變化存儲在哪裏?flutter 引入state的類用於存放中間態,經過調用state.setState()進行此節點及如下的整個子樹更新

State 生命週期

  1. initState(): state create以後被insert到tree時調用的
  2. didUpdateWidget(newWidget):祖先節點rebuild widget時調用
  3. deactivate():widget被remove的時候調用,一個widget從tree中remove掉,能夠在dispose接口被調用前,從新instert到一個新tree中
  4. didChangeDependencies():佈局

    • 初始化時,在initState()以後馬上調用
    • 當依賴的InheritedWidget rebuild,會觸發此接口被調用
  5. 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.
  6. dispose():Widget完全銷燬時調用
  7. reassemble(): hot reload調用

注意事項:優化

  1. A頁面push一個新的頁面B,A頁面的widget樹中的全部state會依次調用deactivate(), didUpdateWidget(newWidget)、build()(這裏懷疑是bug,A頁面push一個新頁面,理論上並無將A頁面進行remove操做),固然從功能上,沒有看出來有什麼異常
  2. 當ListView中的item滾動出可顯示區域的時候,item會被從樹中remove掉,此item子樹中全部的state都會被dispose,state記錄的數據都會銷燬,item滾動回可顯示區域時,會從新建立全新的state、element、renderobject
  3. 使用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 計算

  1. renderObject拿到計算好的size,再加上一些佈局屬性(align、paddig)等,計算child相對parent的offset
  2. offset存放在每一個child renderObject的BoxParentData中
  3. 當parent擁有mutil children時,BoxParentData還用來存children兄弟節點之間的遍歷順序

Relayout boundary

renderObject在layout階段作了Relayout boundary的優化,

>>>>閱讀全文

相關文章
相關標籤/搜索