Flutter渲染流程解析

coderwhy發表於2020-05-28

Widget-Element-RenderObject

一. Flutter的渲染流程

1.1. Widget-Element-RenderObject關係

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

1.2. Widget是什麼?

image-20200302153223929
image-20200302153223929

官方對Widget的說明:

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

自己的理解:

  • 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用來設定內邊距

2.1. Widget

Padding是一個Widget,並且繼承自SingleChildRenderObjectWidget

繼承關係如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
複製程式碼

我們之前在建立Widget時,經常使用StatelessWidget和StatefulWidget,這種Widget只是將其他的Widget在build方法中組裝起來,並不是一個真正可以渲染的Widget(在之前的課程中其實有提到)。

在Padding的類中,我們找不到任何和渲染相關的程式碼,這是因為Padding僅僅作為一個配置資訊,這個配置資訊會隨著我們設定的屬性不同,頻繁的銷燬和建立。

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

  • 並不會,答案在我的另一篇文章中;
  • 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、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注

公眾號
公眾號

相關文章