概述
- Flutter的渲染流程
- 對象的創造過程
- 小部件的鍵
一、Flutter的渲染流程
-
1.1、
Widget的Element和RenderObject
的關系3棵樹的關系 -
1.2、Widget(
窗口小部件
) 是什么?- 官方對Widget的說明:
- Flutter的Widgets的靈感來自React,中心思想是構造你的UI使用這些Widgets。
- Widget使用配置和狀態,描述這個View(界面)應該長什么樣的子。
- 當一個小部件發生改變時,小部件就會重新構建它的描述,框架會和之前的描述進行對比,來決定使用最小的改變(最小變化)在渲染樹中,從一個狀態到另一個狀態。
- 自己的理解:
- 口小部件就是一個個描述文件,這些描述文件在我們進行狀態改變時會不斷的構建。
- 但是對于渲染對象而言,只會使用最小的增量來更新渲染界面。
- 官方對Widget的說明:
-
1.3、Element(
元素
)是什么?Element(`元素`)- 官方對Element的描述:
- Element是一個小部件的實例,在樹中詳細的位置。
- 窗口小部件描述和配置子樹的樣子,而元素實際去配置在元素樹中特定的位置。
- 官方對Element的描述:
-
1.4、RenderObject(渲染對象),官方對RenderObject的描述:
- 渲染樹上的一個對象
- RenderObject層是渲染庫的核心。
二、對象的創造過程
我們這里以Padding為例,Padding設置設置內邊距
-
2.1、Widget(小部件)
填充是一個小部件,并且繼承自SingleChildRenderObjectWidget
繼承關系如下:Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
我們之前在創建Widget時,經常使用StatelessWidget和StatefulWidget,這種Widget只是將其他的Widget在構建方法中組裝起來,并不是一個真正可以渲染的Widget(在之前的課程中其實有提到)。
在Padding的類中,我們發現任何和渲染相關的代碼,這是因為Padding只是一個配置信息,這個配置信息會轉移我們設置的屬性不同,可以替換的銷毀和創建。- 問題:不斷的銷毀和創造會不會影響Flutter的性能呢?
答:并不會,答案在另一篇文章中:你不必擔心Dart的垃圾回收器 - 那么真正的渲染相關的代碼在哪里執行呢?
答:渲染對象
- 問題:不斷的銷毀和創造會不會影響Flutter的性能呢?
-
2.2、渲染對象
我們來看Padding里面的代碼,有一個非常重要的方法:這個方法其實是來自RenderObjectWidget的類,在這個類中它是一個抽象方法;
抽象方法是必須被子類實現的,但是它的子類SingleChildRenderObjectWidget也是一個抽象類,所以可以不實現父類的抽象方法
-
但是Padding不是一個抽象類,必須在這里實現對應的抽象方法,而它的實現就是下面的實現
@override RenderPadding createRenderObject(BuildContext context) { return RenderPadding( padding: padding, textDirection: Directionality.of(context), ); }
頂部的代碼創建了什么呢?RenderPadding的繼承關系是什么呢?
RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
- 我們來具體查看一下RenderPadding的源代碼:
如果預設的_padding和原來保存的value一樣,那么直接返回;
如果絕對,調用_markNeedResolution,而_markNeedResolution內部調用了markNeedsLayout;
而markNeedsLayout的目的就是標記在下一幀布局時,需要重新布局performLayout;
-
如果我們找的是Opacity,那么RenderOpacity是調用markNeedsPaint,RenderOpacity中是有一個油漆方法的;
set padding(EdgeInsetsGeometry value) { assert(value != null); assert(value.isNonNegative); if (_padding == value) return; _padding = value; _markNeedResolution(); } }
-
2.3、Element(元件)
我們來思考一個問題:- 我們寫的大量的Widget在樹結構中存在引用關系,但是Widget會被不斷的銷毀和重建,則意味著這棵樹非常不穩定;
- 那么由誰來維系整個Flutter應用程序的樹形結構的穩定呢?
答案就是元素。 - 官方的描述:Element是一個Widget的實例,在樹中詳細的位置。
元素什么時候創造?在每一次創建Widget的時候,會創建一個對應的Element,然后放入元素插入樹中。
- Element保存著對Widget的引用;
在SingleChildRenderObjectWidget中,我們可以找到如下代碼:
在Widget中,元素被創造,并且在創造時,將this(Widget)設定了;
-
Element就保存了對Widget的應用;
@override SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
在創建完一個元素之后,框架會調用安裝方法來將元素插入到樹中具體的位置:
在調用mount方法時,會同時使用Widget來創建RenderObject,并且保持對RenderObject的引用:
_renderObject = widget.createRenderObject(this);
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _renderObject = widget.createRenderObject(this); assert(() { _debugUpdateRenderObjectOwner(); return true; }()); assert(_slot == newSlot); attachRenderObject(newSlot); _dirty = false; }
但是,如果您去看一下Text這種組合類的小部件,它也會執行mount方法,但是mount方法中并沒有調用createRenderObject這樣的方法。
-
我們發現ComponentElement最主要的目的是掛載之后,調用_firstBuild方法
@override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); assert(_child == null); assert(_active); _firstBuild(); assert(_child != null); } void _firstBuild() { rebuild(); }
如果是一個StatefulWidget,則創建出來的是一個StatefulElement,我們來看一下StatefulElement的構造器:
調用widget的createState()
所以StatefulElement對創建出來的State是有一個引用的
-
而_state又對widget有一個引用
StatefulElement(StatefulWidget widget) : _state = widget.createState(), ....省略代碼 _state._widget = widget;
而調用build的時候,本質上調用的是_state中的build方法:
Widget build() => state.build(this);
-
2.4、build 的 context(上下文) 是什么
在StatelessElement中,我們發現是將這個合并,所以本質上BuildContext就是當前的ElementWidget build() => widget.build(this);
我們來看一下繼承關系圖:元素是實現了BuildContext類(隱式接口)
abstractclass Element extends DiagnosticableTree implements BuildContext
在StatefulElement中,build方法也是類似,調用state的build方式時,引用的是this
Widget build() => state.build(this);
-
2.5、創建過程小結
- 窗口小部件只是描述了配置信息:
- 其中包含createElement方法用于創建元素
- 也包含createRenderObject,但不是自己在調用
- 元素是真正的保存樹結構的對象:
- 創建出來后會由framework調用mount方法;
- 在mount方法中會調用widget的createRenderObject對象;
- 并且Element對widget和RenderObject都有引用;
-
RenderObject 是真正渲染的對象:其中有
markNeedsLayout
、performLayout
、markNeedsPaint
、paint
等方法
- 窗口小部件只是描述了配置信息:
三、小部件的鍵
在我們創造的小部件的時候,總是會看到一個關鍵的參數,它又是做什么的呢?
-
3.1、key的案例需求
key的案例需求class _HYHomePageState extends State<HYHomePage> { List<String> names = ["aaa", "bbb", "ccc"]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Test Key"), ), body: ListView( children: names.map((name) { return ListItemLess(name); }).toList(), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.delete), onPressed: () { setState(() { names.removeAt(0); }); } ), ); } }
-
3.2、StatelessWidget的實現
我們先對ListItem使用一個StatelessWidget進行實現:class ListItemLess extends StatelessWidget { finalString name; final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)); ListItemLess(this.name); @override Widget build(BuildContext context) { return Container( height: 60, child: Text(name), color: randomColor, ); } }
它的實現效果是每刪除一個,所有的顏色都會發現一次變化,原因非常簡單,刪除之后調用setState,會重新構建,重新構建出來的新的StatelessWidget會重新生成一個新的隨機顏色
-
3.3、StatefulWidget的實現(沒有鍵)
class ListItemFul extends StatefulWidget { finalString name; ListItemFul(this.name): super(); @override _ListItemFulState createState() => _ListItemFulState(); } class _ListItemFulState extends State<ListItemFul> { final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)); @override Widget build(BuildContext context) { return Container( height: 60, child: Text(widget.name), color: randomColor, ); } }
我們發現一個很奇怪的現象,顏色不變化,但是數據向上移動了
- 這是因為在刪除第一條數據的時候,Widget對應的元素并沒有改變;
- 而元素中對應的狀態引用也沒有發生改變;
- 在更新Widget的時候,Widget使用了沒有改變的Element中的State;
-
3.4、StatefulWidget的實現(隨機鍵)
我們使用一個隨機的key,ListItemFul的修改如下:class ListItemFul extends StatefulWidget { finalString name; ListItemFul(this.name, {Key key}): super(key: key); @override _ListItemFulState createState() => _ListItemFulState(); }
主頁界面代碼修改如下:
body: ListView( children: names.map((name) { return ListItemFul(name, key: ValueKey(Random().nextInt(10000)),); }).toList(), ),
這一次我們發現,每次刪除都會出現隨機顏色的現象:這是因為修改了key之后,Element會強制刷新,然后對應的State也會重新創建
// Widget類中的代碼 staticbool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
-
3.5、StatefulWidget的實現(名稱為鍵)
這次,我們將名稱作為key來看一下結果body: ListView( children: names.map((name) { return ListItemFul(name, key: ValueKey(name)); }).toList(), ),
我們理想中的效果:
- 因為這是在更新widget的過程中根據key進行了diff算法
- 在前后進行對比時,發現bbb對應的元素和ccc對應的元素會繼續使用,那么就會刪除之前aaa對應的元素,而不是直接刪除最后一個元素
-
3.6、鑰匙的分類
Key本質是一個抽象,不過它也有一個工廠構造器,創建出來一個ValueKey
直接子類主要有:LocalKey和GlobalKeyLocalKey,它具有相同父元素的小部件進行比較,也是diff算法的核心所在;
GlobalKey,通常我們會使用GlobalKey某個Widget對應的Widget或State或Element
-
3.6.1、本地密鑰
LocalKey有三個子類-
ValueKey
:是當我們以特定的值作為鍵時使用,某些一個字符串,數字等等 -
ObjectKey
對象關鍵字:如果兩個學生,他們的名字一樣,使用name作為他們的key就不合適了;我們可以創建出一個學生對象,使用對象來作為key -
UniqueKey
唯一鍵:如果我們要確保key的唯一性,可以使用UniqueKey;例如我們之前使用隨機數來保證key的不同,這里我們就可以換成UniqueKey;
-
-
3.6.2、全局密鑰
GlobalKey可以幫助我們訪問某個Widget的信息,包括Widget或State或Element等對象
我們來看下面的例子:我希望可以在HYHomePage中直接訪問HYHomeContent中的內容class HYHomePage extends StatelessWidget { final GlobalKey<_HYHomeContentState> homeKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表測試"), ), body: HYHomeContent(key: homeKey), floatingActionButton: FloatingActionButton( child: Icon(Icons.data_usage), onPressed: () { print("${homeKey.currentState.value}"); print("${homeKey.currentState.widget.name}"); print("${homeKey.currentContext}"); }, ), ); } } class HYHomeContent extends StatefulWidget { finalString name = "123"; HYHomeContent({Key key}): super(key: key); @override _HYHomeContentState createState() => _HYHomeContentState(); } class _HYHomeContentState extends State<HYHomeContent> { finalString value = "abc"; @override Widget build(BuildContext context) { return Container(); } }