從源碼解讀Flutter構建

關于三棵樹

Flutter 的核心設計思想是一切皆組件 椒拗。Flutter 將組件的概念進行了擴展波丰,把組件的組織和渲染抽象為三部分叨恨,即 Widget识藤,Element 和 RenderObject邪狞。

Widget

Widget 只是一個配置祷蝌,里面存儲的是有關視圖渲染的配置信息,包括布局帆卓、渲染屬性巨朦、事件響應信息等。

Widget 是不可變的剑令,無法更新糊啡,數(shù)據(jù)更新是以重建 Widget 樹的方式進行,會涉及對象的銷毀重建和垃圾回收吁津,所以但是因為只有配置信息棚蓄,不涉及渲染繪制,所以重建的成本很低碍脏。

Element

Element 是 Widget 的一個實例化對象梭依,它承載了視圖構建的上下文數(shù)據(jù),是連接結(jié)構化的配置信息到完成最終渲染的橋梁典尾。

Element 是可變的役拴。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中钾埂,最大程度降低對真實渲染視圖的修改河闰,提高渲染效率科平,而不是銷毀整個渲染視圖樹重建。

當新的 Widget 替換舊的 Widget姜性,導致 Element 變化匠抗,也就是說,多個 Widget 對應一個 Element污抬。

RenderObject

RenderObject 是主要負責實現(xiàn)視圖渲染的對象。

RenderObject 樹在 Flutter 的展示過程分為四個階段绳军,即布局印机、繪制、合成和渲染门驾。 其中射赛,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機制遍歷渲染對象樹奶是,確定樹中各個對象的位置和尺寸楣责,并把它們繪制到不同的圖層上。繪制完畢后聂沙,合成和渲染的工作則交給 Skia 搞定秆麸。

BuildContext

BuildContext 對象實際上是 Element 對象。BuildContext 接口用于阻止對 Element 對象的直接操作及汉。

我們?nèi)粘i_發(fā)中沮趣,一般接觸的都是 Widget,并沒有使用到 Element坷随,其實我們也在一直操作著 Element房铭,BuildContext 對象實際上就是 Element 對象, Element 實現(xiàn)了 BuildContext温眉,告訴了使用者控件在哪里缸匪、可以做什么。BuildContext 接口設計用于阻止對 Element 對象的直接操作类溢。

我們可以把 Widget 當做菜譜凌蔬,Element 是配菜,RenderObject 是燒菜和出菜豌骏。

流程

RenderObjectWidget 介紹

StatelessWidget 和 StatefulWidget 只是用來組裝控件的容器龟梦,并不負責組件最后的布局和繪制。在 Flutter 中窃躲,布局和繪制工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內(nèi)完成的计贰。比如 Text:

class Text extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 返回的是 RichText
    Widget result = RichText(...)
    ...
  }
}

class RichText extends MultiChildRenderObjectWidget{}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}

我們再來看一下 RenderObjectWidget 的源碼,來看看如何使用 Element 和 RenderObject 完成圖形渲染工作蒂窒。


abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}

對于 Element 的創(chuàng)建躁倒,F(xiàn)lutter 會在遍歷 Widget 樹時荞怒,調(diào)用 createElement 去同步 Widget 自身配置,從而生成對應節(jié)點的 Element 對象秧秉。而對于 RenderObject 的創(chuàng)建與更新褐桌,其實是在 RenderObjectElement 類中完成的。


abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
   
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  ...
}

在 Element 創(chuàng)建完畢后象迎,F(xiàn)lutter 會調(diào)用 Element 的 mount 方法荧嵌。在這個方法里,會完成與之關聯(lián)的 RenderObject 對象的創(chuàng)建砾淌,以及與渲染樹的插入工作啦撮,插入到渲染樹后的 Element 就可以顯示到屏幕中了。

構建

mount被調(diào)用汪厨,發(fā)生第一次構建或者 Widget 改變update被調(diào)用赃春,會在rebuild中調(diào)用performRebuild,在此方法中劫乱,父元素調(diào)用updateChild方法织中。在此構建階段,父元素檢查子元素是否可以更新衷戈、刪除或添加到元素樹中狭吼。

  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {}

通過注釋我們知道,這個是 widgets 系統(tǒng)的核心脱惰。每次要根據(jù)更新的配置添加搏嗡、更新或刪除子元素時,都會調(diào)用它拉一。

  • child:由其父元素檢查以在當前構建過程中添加采盒、刪除、更新或重用的元素蔚润。

  • newWidget:子元素將在當前構建過程中引用的小部件 Widget 磅氨。

我們從第一個 case 開始看:

    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }

如果新小部件為null,并且子元素不為空嫡纠,則框架從元素樹中刪除子元素烦租。父元素調(diào)用deactivateChild將子元素置于非活動狀態(tài)的方法,并將子元素的相應渲染對象從渲染樹中分離出來除盏。對應的場景例如:刷新列表后列表數(shù)據(jù)為空叉橱。

再看第二個 case

   if (child != null) {
            ...
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      }
      ...

如果子元素和新小部件都是非空的,并且子元素的舊小部件和新小部件是相同的實例者蠕,那么當前子元素被重用而不需要更新窃祝。因此,不會調(diào)用相應子小部件的構建方法踱侣。就性能而言粪小,這是最理想的情況大磺。也就是負責配置的 Widget 沒有改變,那么會重用這個元素探膊,直接返回杠愧。

再看第三個 case

else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      }

如果子元素和新小部件都是非空的,并且新舊小部件不是同一個實例逞壁,但 canUpdate 方法返回 true流济,通過新的配置更新子元素。對應的場景例如:Text的文本發(fā)生改變后引起的構建腌闯。

再看第四個 case

else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }

如果子元素和新小部件都是非空的袭灯,并且新舊小部件不相同但canUpdate返回false,那么deactivateChild刪除子元素绑嘹,相應的渲染對象與渲染樹分離。最后橘茉,將新小部件的新元素返回到元素樹工腋。這是在性能方面最昂貴的情況,因為實例化了新元素節(jié)點和渲染對象節(jié)點畅卓。對應的場景例如:Text變成了Button擅腰。

再看第五個 case

 else {
      newChild = inflateWidget(newWidget, newSlot);
    }

如果子元素為 null,并且其新子元素為非 null翁潘,那么說明構建階段的樹的此位置有一個新的小部件趁冈。因此,父元素首先調(diào)用inflateWidget拜马,在其中調(diào)用新小部件方法的createElement方法渗勘,并返回新小部件的新元素。此時俩莽,父級也為創(chuàng)建的元素設置了一個槽旺坠。對應的場景例如:在空的row里添加了一個Button

當然扮超,第一個構建時取刃,都會走到這個 case

更新

StatefulWidget被加載時出刷,StatefulElement會被創(chuàng)建璧疗,StatefulElement的構造方法中,通過createState方法創(chuàng)建State馁龟,并將StatefulWidgetState對象將永久關聯(lián)崩侠。

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),

mount中,調(diào)用_firstBuild屁柏,然后調(diào)用state.initState() 啦膜,初始化State有送,并且之后調(diào)用一次didChangeDependencies

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    ...
    _firstBuild();
        ...
  }
  @override
  void _firstBuild() {
    assert(state._debugLifecycleState == _StateLifecycle.created);
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
      ...
      state.didChangeDependencies();
            ...
            }

而在上面第三個case中僧家,child.update(newWidget)會先調(diào)用state.didUpdateWidget(oldWidget)雀摘,再build,所以我們重寫didUpdateWidget方法時八拱,State由于保證該build方法會在didUpdateWidget之后被調(diào)用阵赠,因此無需在didUpdateWidget中顯式觸發(fā)構建。

 @override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
        ...
    final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
        ...
    rebuild();
  }
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肌稻,一起剝皮案震驚了整個濱河市清蚀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爹谭,老刑警劉巖枷邪,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诺凡,居然都是意外死亡东揣,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門腹泌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嘶卧,“玉大人,你說我怎么就攤上這事凉袱〗嬉鳎” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵专甩,是天一觀的道長钟鸵。 經(jīng)常有香客問我,道長涤躲,這世上最難降的妖魔是什么携添? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮篓叶,結(jié)果婚禮上烈掠,老公的妹妹穿的比我還像新娘。我一直安慰自己缸托,他們只是感情好左敌,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俐镐,像睡著了一般矫限。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天叼风,我揣著相機與錄音取董,去河邊找鬼。 笑死无宿,一個胖子當著我的面吹牛茵汰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播孽鸡,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蹂午,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了彬碱?” 一聲冷哼從身側(cè)響起豆胸,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巷疼,沒想到半個月后晚胡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嚼沿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年搬泥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伏尼。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖尉尾,靈堂內(nèi)的尸體忽然破棺而出爆阶,到底是詐尸還是另有隱情,我是刑警寧澤沙咏,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布辨图,位于F島的核電站,受9級特大地震影響肢藐,放射性物質(zhì)發(fā)生泄漏故河。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一吆豹、第九天 我趴在偏房一處隱蔽的房頂上張望鱼的。 院中可真熱鬧,春花似錦痘煤、人聲如沸凑阶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宙橱。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間师郑,已是汗流浹背环葵。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宝冕,地道東北人张遭。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像猬仁,于是被迫代替她去往敵國和親帝璧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

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