Flutter渲染流程解析

Widget-Element-RenderObject

一. Flutter的渲染流程

1.1. Widget-Element-RenderObject關係

3棵tree的關係
3棵tree的關係

1.2. Widget是什麼?

image-20200302153223929
image-20200302153223929

官方對Widget的說明:vue

  • Flutter的Widgets的靈感來自React,中心思想是構造你的UI使用這些Widgets。
  • Widget使用配置和狀態,描述這個View(界面)應該長什麼樣子。
  • 當一個Widget發生改變時,Widget就會從新build它的描述,框架會和以前的描述進行對比,來決定使用最小的改變(minimal changes)在渲染樹中,從一個狀態到另外一個狀態。

本身的理解:web

  • Widget就是一個個描述文件,這些描述文件在咱們進行狀態改變時會不斷的build。
  • 可是對於渲染對象來講,只會使用最小的開銷來更新渲染界面。

1.3. Element是什麼?

image-20200302154618370
image-20200302154618370

官方對Element的描述:算法

  • Element是一個Widget的實例,在樹中詳細的位置。
  • Widget描述和配置子樹的樣子,而Element實際去配置在Element樹中特定的位置。

1.4. RenderObject

image-20200302155014847
image-20200302155014847

官方對RenderObject的描述:數據結構

  • 渲染樹上的一個對象
  • RenderObject層是渲染庫的核心。

二. 對象的建立過程

咱們這裏以Padding爲例,Padding用來設置內邊距app

2.1. Widget

Padding是一個Widget,而且繼承自SingleChildRenderObjectWidget框架

繼承關係以下:less

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
複製代碼

咱們以前在建立Widget時,常用StatelessWidget和StatefulWidget,這種Widget只是將其餘的Widget在build方法中組裝起來,並非一個真正能夠渲染的Widget(在以前的課程中其實有提到)。dom

在Padding的類中,咱們找不到任何和渲染相關的代碼,這是由於Padding僅僅做爲一個配置信息,這個配置信息會隨着咱們設置的屬性不一樣,頻繁的銷燬和建立。編輯器

問題:頻繁的銷燬和建立會不會影響Flutter的性能呢?ide

  • 並不會,答案在個人另外一篇文章中;
  • https://mp.weixin.qq.com/s/J4XoXJHJSmn8VaMoz3BZJQ

那麼真正的渲染相關的代碼在哪裏執行呢?

  • RenderObject

2.2. RenderObject

咱們來看Padding裏面的代碼,有一個很是重要的方法:

  • 這個方法實際上是來自RenderObjectWidget的類,在這個類中它是一個抽象方法;
  • 抽象方法是必須被子類實現的,可是它的子類SingleChildRenderObjectWidget也是一個抽象類,因此能夠不實現父類的抽象方法
  • 可是Padding不是一個抽象類,必須在這裏實現對應的抽象方法,而它的實現就是下面的實現
@override
RenderPadding createRenderObject(BuildContext context) {  return RenderPadding(  padding: padding,  textDirection: Directionality.of(context),  ); } 複製代碼

上面的代碼建立了什麼呢?RenderPadding

RenderPadding的繼承關係是什麼呢?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
複製代碼

咱們來具體查看一下RenderPadding的源代碼:

  • 若是傳入的_padding和原來保存的value同樣,那麼直接return;
  • 若是不一致,調用_markNeedResolution,而_markNeedResolution內部調用了markNeedsLayout;
  • 而markNeedsLayout的目的就是標記在下一幀繪製時,須要從新佈局performLayout;
  • 若是咱們找的是Opacity,那麼RenderOpacity是調用markNeedsPaint,RenderOpacity中是有一個paint方法的;
set padding(EdgeInsetsGeometry value) {
 assert(value != null);  assert(value.isNonNegative);  if (_padding == value)  return;  _padding = value;  _markNeedResolution();  } 複製代碼

2.3. Element

咱們來思考一個問題:

  • 以前咱們寫的大量的Widget在樹結構中存在引用關係,可是Widget會被不斷的銷燬和重建,那麼意味着這棵樹很是不穩定;
  • 那麼由誰來維繫整個Flutter應用程序的樹形結構的穩定呢?
  • 答案就是Element。
  • 官方的描述:Element是一個Widget的實例,在樹中詳細的位置。

Element何時建立?

在每一次建立Widget的時候,會建立一個對應的Element,而後將該元素插入樹中。

  • Element保存着對Widget的引用;

在SingleChildRenderObjectWidget中,咱們能夠找到以下代碼:

  • 在Widget中,Element被建立,而且在建立時,將this(Widget)傳入了;
  • Element就保存了對Widget的應用;
@override
 SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this); 複製代碼

在建立完一個Element以後,Framework會調用mount方法來將Element插入到樹中具體的位置:

mount方法
mount方法

在調用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這種組合類的Widget,它也會執行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中,咱們發現是將this傳入,因此本質上BuildContext就是當前的Element

Widget build() => widget.build(this);
複製代碼

咱們來看一下繼承關係圖:

  • Element是實現了BuildContext類(隱式接口)
abstract class Element extends DiagnosticableTree implements BuildContext 複製代碼

在StatefulElement中,build方法也是相似,調用state的build方式時,傳入的是this

Widget build() => state.build(this);
複製代碼

2.5. 建立過程小結

Widget只是描述了配置信息:

  • 其中包含createElement方法用於建立Element
  • 也包含createRenderObject,可是不是本身在調用

Element是真正保存樹結構的對象:

  • 建立出來後會由framework調用mount方法;
  • 在mount方法中會調用widget的createRenderObject對象;
  • 而且Element對widget和RenderObject都有引用;

RenderObject是真正渲染的對象:

  • 其中有 markNeedsLayout performLayout markNeedsPaint paint等方法

三. Widget的key

在咱們建立Widget的時候,老是會看到一個key的參數,它又是作什麼的呢?

3.1. key的案例需求

咱們一塊兒來作一個key的案例需求

key的案例需求
key的案例需求

home界面的基本代碼:

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);  });  }  ),  );  } } 複製代碼

注意:待會兒咱們會修改返回的ListItem爲ListItemLess或者ListItemFul

3.2. StatelessWidget的實現

咱們先對ListItem使用一個StatelessWidget進行實現:

class ListItemLess extends StatelessWidget {
 final String 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,會從新build,從新build出來的新的StatelessWidget會從新生成一個新的隨機顏色
image-20200320151331285
image-20200320151331285

3.3. StatefulWidget的實現(沒有key)

咱們對ListItem使用StatefulWidget來實現

class ListItemFul extends StatefulWidget {
 final String 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對應的Element並無改變;
  • 而Element中對應的State引用也沒有發生改變;
  • 在更新Widget的時候,Widget使用了沒有改變的Element中的State;
image-20200320151747199
image-20200320151747199

3.4. StatefulWidget的實現(隨機key)

咱們使用一個隨機的key

ListItemFul的修改以下:

class ListItemFul extends StatefulWidget {
 final String name;  ListItemFul(this.name, {Key key}): super(key: key);  @override  _ListItemFulState createState() => _ListItemFulState(); } 複製代碼

home界面代碼修改以下:

body: ListView(
 children: names.map((name) {  return ListItemFul(name, key: ValueKey(Random().nextInt(10000)),);  }).toList(), ), 複製代碼

這一次咱們發現,每次刪除都會出現隨機顏色的現象:

  • 這是由於修改了key以後,Element會強制刷新,那麼對應的State也會從新建立
// Widget類中的代碼
static bool canUpdate(Widget oldWidget, Widget newWidget) {  return oldWidget.runtimeType == newWidget.runtimeType  && oldWidget.key == newWidget.key; } 複製代碼
image-20200320152321905
image-20200320152321905

3.5. StatefulWidget的實現(name爲key)

此次,咱們將name做爲key來看一下結果:

body: ListView(
 children: names.map((name) {  return ListItemFul(name, key: ValueKey(name));  }).toList(), ), 複製代碼

咱們理想中的效果:

  • 由於這是在更新widget的過程當中根據key進行了diff算法
  • 在先後進行對比時,發現bbb對應的Element和ccc對應的Element會繼續使用,那麼就會刪除以前aaa對應的Element,而不是直接刪除最後一個Element
image-20200320152610235
image-20200320152610235

3.6. Key的分類

Key自己是一個抽象,不過它也有一個工廠構造器,建立出來一個ValueKey

直接子類主要有:LocalKey和GlobalKey

  • LocalKey,它應用於具備相同父Element的Widget進行比較,也是diff算法的核心所在;
  • GlobalKey,一般咱們會使用GlobalKey某個Widget對應的Widget或State或Element

3.6.1. LocalKey

LocalKey有三個子類

ValueKey:

  • ValueKey是當咱們以特定的值做爲key時使用,好比一個字符串、數字等等

ObjectKey:

  • 若是兩個學生,他們的名字同樣,使用name做爲他們的key就不合適了
  • 咱們能夠建立出一個學生對象,使用對象來做爲key

UniqueKey

  • 若是咱們要確保key的惟一性,可使用UniqueKey;
  • 好比咱們以前使用隨機數來保證key的不一樣,這裏咱們就能夠換成UniqueKey;

3.6.2. GlobalKey

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 {  final String name = "123";   HYHomeContent({Key key}): super(key: key);   @override  _HYHomeContentState createState() => _HYHomeContentState(); }  class _HYHomeContentState extends State<HYHomeContent> {  final String value = "abc";   @override  Widget build(BuildContext context) {  return Container();  } } 複製代碼

備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號
公衆號
相關文章
相關標籤/搜索