flutter 三棵樹(shù) widget浮还,element,renderObject

前幾篇中介紹了flutter整體框架以及dart語(yǔ)言基礎(chǔ)和特性闽巩。從這篇開(kāi)始分享flutter的相關(guān)內(nèi)容霹琼。本篇要分享的是flutter框架里的三棵樹(shù)widget俭识,element,renderObject。將從以下幾個(gè)問(wèn)題逐步探究這三棵樹(shù):

  • widget尖阔,element顿仇,renderObject對(duì)應(yīng)關(guān)系
  • 三棵樹(shù)是如何工作的寄症?
    Flutter的理念是一切都是Widget(Everything is Widget)绣硝。開(kāi)發(fā)者在開(kāi)發(fā)Flutter app的時(shí)候主要都是在寫(xiě)很多Widget。對(duì)flutter有所了解的都知道flutter是聲明式UI框架叔营,與之對(duì)應(yīng)的是命令式屋彪。舉個(gè)例子:一個(gè)頁(yè)面上有N個(gè)TextView,在Android開(kāi)發(fā)中如果我們想給這N個(gè)TextView設(shè)置文案绒尊,那我們通常需要調(diào)用這N個(gè)TextView的setText方法來(lái)設(shè)置畜挥。而對(duì)于聲明式UI框架的Flutter,需要將數(shù)據(jù)變更婴谱,重新構(gòu)建WidgetTree蟹但。也就是我們?cè)趂lutter開(kāi)發(fā)過(guò)程中數(shù)據(jù)變更時(shí)做的setState。相信你一定會(huì)想到每次setState都會(huì)重新構(gòu)建WidgetTree勘究,應(yīng)該對(duì)性能損耗很大吧。flutter號(hào)稱高性能的跨平臺(tái)UI框架斟冕,那么是怎么解決這樣的問(wèn)題呢口糕?
    我們?cè)谑褂肳idget是都是widget的構(gòu)造方法,另外我們使用的Widget大部分是繼承自 StateLessWidget或者StatefulWidget磕蛇。我們先看看StateLessWidget里都做了些什么呢景描?
abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatelessElement createElement() => StatelessElement(this);

  /// Describes the part of the user interface represented by this widget.
  @protected
  Widget build(BuildContext context);
}

通過(guò)這段源碼我們不難看出十办,在createElement 方法里,widget將自己的引用傳給了StatelessElement超棺,我們?cè)诳聪耂tatelessElement:

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

在這塊代碼中build()方法將widget跟element關(guān)聯(lián)了起來(lái)向族,也就是widget的build方法持有的buildContext就是element。同時(shí)我們也可以發(fā)現(xiàn)并沒(méi)有繪制棠绘,布局相關(guān)的內(nèi)容件相,那只能繼續(xù)跟進(jìn)父類ComponentElement,Element了氧苍,跟進(jìn)之后發(fā)現(xiàn)我們只能找到get renderObject卻并沒(méi)找到renderObject的創(chuàng)建夜矗,但是在Element里有這么一塊代碼:

/// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  RenderObject? get renderObject {
    RenderObject? result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element._lifecycleState == _ElementLifecycle.defunct) {
        return;
      } else if (element is RenderObjectElement) {
        result = element.renderObject;
      } else {
        element.visitChildren(visit);
      }
    }
    visit(this);
    return result;
  }

在獲取的時(shí)候?qū)嶋H上是在element 樹(shù)上去查找離當(dāng)前節(jié)點(diǎn)最近的RenderObjectElement,也就是說(shuō)此處返回的RenderObject是RenderObjectElement的element.renderObject 让虐。到這里我們也可以得到一個(gè)結(jié)論就是:Widget 跟 Element是一一對(duì)應(yīng)的紊撕,但是Element跟RenderObject并不是一一對(duì)應(yīng)的。下面我們來(lái)看下RenderObjectElement:

@override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(() {
      _debugDoingBuild = true;
      return true;
    }());
    _renderObject = (widget as RenderObjectWidget).createRenderObject(this); /// 此處會(huì)去創(chuàng)建RenderObject
    assert(!_renderObject!.debugDisposed!);
    assert(() {
      _debugDoingBuild = false;
      return true;
    }());
    assert(() {
      _debugUpdateRenderObjectOwner();
      return true;
    }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

mount方法在生命周期里有講過(guò)赡突,會(huì)在頁(yè)面創(chuàng)建的時(shí)候調(diào)用对扶,也是在此時(shí)renderObject跟RenderObjectElement,RenderObjectWidget也就關(guān)聯(lián)上了惭缰,并且RenderObject持有element的引用浪南。那么接下來(lái)毫無(wú)疑問(wèn)看看RenderObject是干啥的吧。

/// Paint this render object into the given context at the given offset.
  ///
  /// Subclasses should override this method to provide a visual appearance
  /// for themselves. The render object's local coordinate system is
  /// axis-aligned with the coordinate system of the context's canvas and the
  /// render object's local origin (i.e, x=0 and y=0) is placed at the given
  /// offset in the context's canvas.
  ///
  /// Do not call this function directly. If you wish to paint yourself, call
  /// [markNeedsPaint] instead to schedule a call to this function. If you wish
  /// to paint one of your children, call [PaintingContext.paintChild] on the
  /// given `context`.
  ///
  /// When painting one of your children (via a paint child function on the
  /// given context), the current canvas held by the context might change
  /// because draw operations before and after painting children might need to
  /// be recorded on separate compositing layers.
  void paint(PaintingContext context, Offset offset) { }
void layout(Constraints constraints, { bool parentUsesSize = false }) 

在RenderObject中很容易就找到了跟布局相關(guān)的layout方法从媚,和跟繪制相關(guān)的paint方法逞泄,從而我們可以得出一個(gè)結(jié)論:就是RenderObject其實(shí)是真正做繪制布局相關(guān)操作的對(duì)象。
下面我們總結(jié)下widget,element,renderObject三者之間的關(guān)系:


image.png
  • widget是面對(duì)開(kāi)發(fā)者使用的配置對(duì)象拜效,可以通過(guò)widget對(duì)實(shí)際繪制做相關(guān)的配置和描述喷众,比價(jià)輕量級(jí)
  • element 是Widget在UI樹(shù)具體位置的一個(gè)實(shí)例化對(duì)象,是實(shí)際處理生命周期紧憾,UI樹(shù)位置相關(guān)的對(duì)象
  • RenderObject是實(shí)際布局和繪制的對(duì)象
    其中widget跟Element是一一對(duì)應(yīng)到千,但是跟RenderObject并非一一對(duì)應(yīng),在實(shí)際開(kāi)發(fā)中赴穗,一般renderObject要少憔四。
    (在這里我們也可以思考下,如果沒(méi)有Widget直接將Element暴漏出去供大家使用會(huì)有什么問(wèn)題呢般眉,少了一層結(jié)構(gòu)會(huì)不會(huì)更簡(jiǎn)單呢了赵,為啥要設(shè)計(jì)成這種結(jié)構(gòu)呢?)
    知道了三者之間關(guān)系那么我們下面我們繼續(xù)針對(duì)flutter這種架構(gòu)方式是如何做到優(yōu)化的甸赃?
    我們?cè)?a href="http://www.reibang.com/p/904be3cf4db0" target="_blank">生命周期篇中有提到在創(chuàng)建或者更新時(shí)會(huì)執(zhí)行elemnet的如下方法:
class ComponentElement extends Element{

@override
void performRebuild() {
………
Widget build;

build = build();

………
_child = updateChild(_child, build, slot);
…………
}
}

接下來(lái)我們著重看下updateChild方法:

 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) { /// 如果新的widget是null
      if (child != null) ///child 不是null柿汛,那么就將該elemnt從element tree上移除
        deactivateChild(child);
      return null;
    }

///如果新的widget不是null時(shí)
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      // When the type of a widget is changed between Stateful and Stateless via
      // hot reload, the element tree will end up in a partially invalid state.
      // That is, if the widget was a StatefulWidget and is now a StatelessWidget,
      // then the element tree currently contains a StatefulElement that is incorrectly
      // referencing a StatelessWidget (and likewise with StatelessElement).
      //
      // To avoid crashing due to type errors, we need to gently guide the invalid
      // element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
      // returns false which prevents us from trying to update the existing element
      // incorrectly.
      //
      // For the case where the widget becomes Stateful, we also need to avoid
      // accessing `StatelessElement.widget` as the cast on the getter will
      // cause a type error to be thrown. Here we avoid that by short-circuiting
      // the `Widget.canUpdate` check once `hasSameSuperclass` is false.
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) { // 兩個(gè)widget誰(shuí)同一個(gè)的話
        // We don't insert a timeline event here, because otherwise it's
        // confusing that widgets that "don't update" (because they didn't
        // change) get "charged" on the timeline.
        if (child.slot != newSlot) /// 如果位置不一樣更新element tree的位置
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) { 
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
        if (isTimelineTracked) {
          Map<String, String> debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
          assert(() {
            if (kDebugMode) {
              debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments();
            }
            return true;
          }());
          Timeline.startSync(
            '${newWidget.runtimeType}',
            arguments: debugTimelineArguments,
          );
        }
        child.update(newWidget);
        if (isTimelineTracked)
          Timeline.finishSync();
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else { // 如果完全不一樣
        deactivateChild(child);
        assert(child._parent == null);
        // The [debugProfileBuildsEnabled] code for this branch is inside
        // [inflateWidget], since some [Element]s call [inflateWidget] directly
        // instead of going through [updateChild].
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else { // 之前的widget就是null,那么就新inflate一個(gè)
      // The [debugProfileBuildsEnabled] code for this branch is inside
      // [inflateWidget], since some [Element]s call [inflateWidget] directly
      // instead of going through [updateChild].
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        assert(owner != null);
        owner!._debugReserveGlobalKeyFor(this, newChild, key);
      }
      return true;
    }());

    return newChild;
  }

這里著重看下canUpdate埠对,后面開(kāi)發(fā)對(duì)理解widget的更新有幫助

/// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

下面我們用張表來(lái)總結(jié)上面更新的各種情況:


image.png

到這里我們基本把widget络断,element裁替,renderObject三者的關(guān)系,以及如何優(yōu)化的做了一定的分析貌笨。你以為flutter就只做了這些嗎弱判,只做了這些就稱得上是高效能的UI框架了嗎?后面我們會(huì)繼續(xù)分析flutter的layer以及fluttet渲染相關(guān)內(nèi)容锥惋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末昌腰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子净刮,更是在濱河造成了極大的恐慌剥哑,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淹父,死亡現(xiàn)場(chǎng)離奇詭異株婴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暑认,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)困介,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蘸际,你說(shuō)我怎么就攤上這事座哩。” “怎么了粮彤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵根穷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我导坟,道長(zhǎng)屿良,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任惫周,我火速辦了婚禮尘惧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘递递。我一直安慰自己喷橙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布登舞。 她就那樣靜靜地躺著贰逾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪菠秒。 梳的紋絲不亂的頭發(fā)上疙剑,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼核芽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酵熙,可吹牛的內(nèi)容都是我干的轧简。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼匾二,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哮独!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起察藐,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皮璧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后分飞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體悴务,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年譬猫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讯檐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡染服,死狀恐怖别洪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柳刮,我是刑警寧澤挖垛,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站秉颗,受9級(jí)特大地震影響痢毒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜站宗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一闸准、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧梢灭,春花似錦夷家、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至钥顽,卻和暖如春义屏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工闽铐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝶怔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓兄墅,卻偏偏與公主長(zhǎng)得像踢星,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子隙咸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容