Day10 - Flutter的Widget-Element-RenderObject

概述

  • Flutter的渲染流程
  • 對象的創造過程
  • 小部件的鍵
一、Flutter的渲染流程
  • 1.1、Widget的Element和RenderObject 的關系

    3棵樹的關系
  • 1.2、Widget(窗口小部件) 是什么?

    • 官方對Widget的說明:
      • Flutter的Widgets的靈感來自React,中心思想是構造你的UI使用這些Widgets。
      • Widget使用配置和狀態,描述這個View(界面)應該長什么樣的子。
      • 當一個小部件發生改變時,小部件就會重新構建它的描述,框架會和之前的描述進行對比,來決定使用最小的改變(最小變化)在渲染樹中,從一個狀態到另一個狀態。
    • 自己的理解:
      • 口小部件就是一個個描述文件,這些描述文件在我們進行狀態改變時會不斷的構建。
      • 但是對于渲染對象而言,只會使用最小的增量來更新渲染界面。
  • 1.3、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的垃圾回收器
    • 那么真正的渲染相關的代碼在哪里執行呢?
      答:渲染對象
  • 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就是當前的Element

    Widget 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、markNeedsPaintpaint 等方法
三、小部件的鍵

在我們創造的小部件的時候,總是會看到一個關鍵的參數,它又是做什么的呢?

  • 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和GlobalKey

    • LocalKey,它具有相同父元素的小部件進行比較,也是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();
           }
      }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370