Flutter開發(fā) -- [13 - 渲染流程]

一. Flutter的渲染流程

1.1. Widget-Element-RenderObject關系

1.2. Widget是什么没佑?

官方對Widget的說明:

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

自己的理解:

  • Widget就是一個個描述文件胞谭,這些描述文件在我們進行狀態(tài)改變時會不斷的build垃杖。
  • 但是對于渲染對象來說,只會使用最小的開銷來更新渲染界面丈屹。

1.3. Element是什么调俘?

官方對Element的描述:

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

1.4. RenderObject

官方對RenderObject的描述:

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

二. 對象的創(chuàng)建過程

我們這里以Padding為例侧巨,Padding用來設置內邊距

2.1. Widget

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

繼承關系如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

我們之前在創(chuàng)建Widget時鞭达,經常使用StatelessWidget和StatefulWidget司忱,這種Widget只是將其他的Widget在build方法中組裝起來,并不是一個真正可以渲染的Widget(在之前的課程中其實有提到)畴蹭。

在Padding的類中坦仍,我們找不到任何和渲染相關的代碼,這是因為Padding僅僅作為一個配置信息叨襟,這個配置信息會隨著我們設置的屬性不同繁扎,頻繁的銷毀和創(chuàng)建。

問題:頻繁的銷毀和創(chuàng)建會不會影響Flutter的性能呢糊闽?

  • 并不會

那么真正的渲染相關的代碼在哪里執(zhí)行呢梳玫?

  • RenderObject

2.2. RenderObject

我們來看Padding里面的代碼,有一個非常重要的方法:

  • 這個方法其實是來自RenderObjectWidget的類右犹,在這個類中它是一個抽象方法提澎;
  • 抽象方法是必須被子類實現的,但是它的子類SingleChildRenderObjectWidget也是一個抽象類念链,所以可以不實現父類的抽象方法
  • 但是Padding不是一個抽象類盼忌,必須在這里實現對應的抽象方法,而它的實現就是下面的實現
@override
RenderPadding createRenderObject(BuildContext context) {
  return RenderPadding(
    padding: padding,
    textDirection: Directionality.of(context),
  );
}

上面的代碼創(chuàng)建了什么呢掂墓?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會被不斷的銷毀和重建,那么意味著這棵樹非常不穩(wěn)定力崇;
  • 那么由誰來維系整個Flutter應用程序的樹形結構的穩(wěn)定呢斗塘?
  • 答案就是Element。
  • 官方的描述:Element是一個Widget的實例亮靴,在樹中詳細的位置馍盟。

Element什么時候創(chuàng)建?

在每一次創(chuàng)建Widget的時候茧吊,會創(chuàng)建一個對應的Element贞岭,然后將該元素插入樹中。

  • Element保存著對Widget的引用搓侄;

在SingleChildRenderObjectWidget中瞄桨,我們可以找到如下代碼:

  • 在Widget中,Element被創(chuàng)建讶踪,并且在創(chuàng)建時芯侥,將this(Widget)傳入了;
  • Element就保存了對Widget的應用乳讥;
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);

在創(chuàng)建完一個Element之后柱查,Framework會調用mount方法來將Element插入到樹中具體的位置:

在調用mount方法時,會同時使用Widget來創(chuàng)建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,它也會執(zhí)行mount方法留晚,但是mount方法中并沒有調用createRenderObject這樣的方法酵紫。

  • 我們發(fā)現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错维,則創(chuàng)建出來的是一個StatefulElement

我們來看一下StatefulElement的構造器:

  • 調用widget的createState()
  • 所以StatefulElement對創(chuàng)建出來的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中,我們發(fā)現是將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. 創(chuàng)建過程小結

Widget只是描述了配置信息:

  • 其中包含createElement方法用于創(chuàng)建Element
  • 也包含createRenderObject臭挽,但是不是自己在調用

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

  • 創(chuàng)建出來后會由framework調用mount方法欢峰;
  • 在mount方法中會調用widget的createRenderObject對象纽帖;
  • 并且Element對widget和RenderObject都有引用懊直;

RenderObject是真正渲染的對象:

  • 其中有markNeedsLayout performLayout markNeedsPaint paint等方法

三. Widget的key

在我們創(chuàng)建Widget的時候室囊,總是會看到一個key的參數融撞,它又是做什么的呢?

3.1. key的案例需求

我們一起來做一個key的案例需求

<figcaption style="margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">key的案例需求</figcaption>

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,
    );
  }
}

它的實現效果是每刪除一個冬念,所有的顏色都會發(fā)現一次變化

  • 原因非常簡單,刪除之后調用setState醒陆,會重新build刨摩,重新build出來的新的StatelessWidget會重新生成一個新的隨機顏色

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,
    );
  }
}

我們發(fā)現一個很奇怪的現象澡刹,顏色不變化罢浇,但是數據向上移動了

  • 這是因為在刪除第一條數據的時候,Widget對應的Element并沒有改變攒岛;
  • 而Element中對應的State引用也沒有發(fā)生改變灾锯;
  • 在更新Widget的時候挠进,Widget使用了沒有改變的Element中的State誊册;

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(),
),

這一次我們發(fā)現,每次刪除都會出現隨機顏色的現象:

  • 這是因為修改了key之后嘲碱,Element會強制刷新局蚀,那么對應的State也會重新創(chuàng)建
// Widget類中的代碼
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}
image-20200320152321905

<figcaption style="margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">image-20200320152321905</figcaption>

3.5. StatefulWidget的實現(name為key)

這次扶欣,我們將name作為key來看一下結果:

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

我們理想中的效果:

  • 因為這是在更新widget的過程中根據key進行了diff算法
  • 在前后進行對比時料祠,發(fā)現bbb對應的Element和ccc對應的Element會繼續(xù)使用髓绽,那么就會刪除之前aaa對應的Element顺呕,而不是直接刪除最后一個Element

3.6. Key的分類

Key本身是一個抽象株茶,不過它也有一個工廠構造器忌卤,創(chuàng)建出來一個ValueKey

直接子類主要有:LocalKey和GlobalKey

  • LocalKey驰徊,它應用于具有相同父Element的Widget進行比較,也是diff算法的核心所在颗味;
  • GlobalKey浦马,通常我們會使用GlobalKey某個Widget對應的Widget或State或Element

3.6.1. LocalKey

LocalKey有三個子類

ValueKey:

  • ValueKey是當我們以特定的值作為key時使用晶默,比如一個字符串航攒、數字等等

ObjectKey:

  • 如果兩個學生漠畜,他們的名字一樣憔狞,使用name作為他們的key就不合適了
  • 我們可以創(chuàng)建出一個學生對象,使用對象來作為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();
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末厢洞,一起剝皮案震驚了整個濱河市躺翻,隨后出現的幾起案子公你,更是在濱河造成了極大的恐慌迂尝,老刑警劉巖剪芥,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溉躲,死亡現場離奇詭異,居然都是意外死亡偏塞,警方通過查閱死者的電腦和手機邦鲫,發(fā)現死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門古今,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捉腥,“玉大人你画,你說我怎么就攤上這事坏匪∈首遥” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵苦囱,是天一觀的道長沿彭。 經常有香客問我喉刘,道長漆弄,這世上最難降的妖魔是什么睦裳? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮撼唾,結果婚禮上廉邑,老公的妹妹穿的比我還像新娘。我一直安慰自己倒谷,他們只是感情好蛛蒙,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渤愁,像睡著了一般牵祟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抖格,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天收奔,我揣著相機與錄音,去河邊找鬼。 笑死摧阅,一個胖子當著我的面吹牛比规,可吹牛的內容都是我干的。 我是一名探鬼主播霎冯,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缠俺,長吁一口氣:“原來是場噩夢啊……” “哼偿警!你這毒婦竟也來了睁本?” 一聲冷哼從身側響起枉疼,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤航闺,失蹤者是張志新(化名)和其女友劉穎懈叹,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡通今,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了褂乍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡哥艇,死狀恐怖哩俭,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情垦藏,我是刑警寧澤弟灼,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布冒黑,位于F島的核電站,受9級特大地震影響泵殴,放射性物質發(fā)生泄漏。R本人自食惡果不足惜瘫析,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桃序。 院中可真熱鬧,春花似錦葛账、人聲如沸趋急。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琼牧。三九已至恢筝,卻和暖如春哀卫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背撬槽。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工此改, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侄柔。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓带斑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勋拟。 傳聞我的和親對象是個殘疾皇子勋磕,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內容