Flutter 1.17 中的導航解密和性能提升

Flutter 1.17 對比上一個穩(wěn)定版本镣屹,更多是帶來了性能上的提升,其中一個關(guān)鍵的優(yōu)化點就是 Navigator 的內(nèi)部邏輯荐绝,本篇將帶你解密 Navigator 從 1.12 到 1.17 的變化鹊汛,并介紹 Flutter 1.17 上究竟優(yōu)化了哪些性能。

一并淋、Navigator 優(yōu)化了什么?

在 1.17 版本最讓人感興趣的變動莫過于:“打開新的不透明頁面之后珍昨,路由里的舊頁面不會再觸發(fā) build县耽。

雖然之前介紹過 build 方法本身很輕,但是在“不需要”的時候“不執(zhí)行”明顯更符合我們的預期镣典,而這個優(yōu)化的 PR 主要體現(xiàn)在 stack.dartoverlay.dart 兩個文件上兔毙。

  • stack.dart 文件的修改,只是為了將 RenderStack 的相關(guān)邏輯變?yōu)楣蚕淼撵o態(tài)方法 getIntrinsicDimensionlayoutPositionedChild 兄春,其實就是共享 Stack 的部分布局能力給 Overlay 澎剥。

  • overlay.dart 文件的修改則是這次的靈魂所在。

二赶舆、Navigator 的 Overlay

事實上我們常用的 Navigator 是一個 StatefulWidget哑姚, 而常用的 poppush 等方法對應的邏輯都是在 NavigatorState 中芜茵,而 NavigatorState 主要是通過 Overlay 來承載路由頁面叙量,所以導航頁面間的管理邏輯主要在于 Overlay

2.1九串、Overlay 是什么宛乃?

Overlay 大家可能用過,在 Flutter 中可以通過 Overlay 來向 MaterialApp 添加全局懸浮控件蒸辆,這是因為Overlay 是一個類似 Stack 層級控件,但是它可以通過 OverlayEntry 來獨立地管理內(nèi)部控件的展示析既。

比如可以通過 overlayState.insert 插入一個 OverlayEntry 來實現(xiàn)插入一個圖層躬贡,而OverlayEntrybuilder 方法會在展示時被調(diào)用,從而出現(xiàn)需要的布局效果眼坏。

    var overlayState = Overlay.of(context);
    var _overlayEntry = new OverlayEntry(builder: (context) {
      return new Material(
        color: Colors.transparent,
        child: Container(
          child: Text(
            "${widget.platform} ${widget.deviceInfo} ${widget.language} ${widget.version}",
            style: TextStyle(color: Colors.white, fontSize: 10),
          ),
        ),
      );
    });
    overlayState.insert(_overlayEntry);

2.2拂玻、Overlay 如何實現(xiàn)導航?

Navigator 中其實也是使用了 Overlay 實現(xiàn)頁面管理宰译,每個打開的 Route 默認情況下是向 Overlay 插入了兩個 OverlayEntry檐蚜。

為什么是兩個后面會介紹。

而在 Overlay 中沿侈, List<OverlayEntry> _entries 的展示邏輯又是通過 _Theatre 來完成的闯第,在 _Theatre 中有 onstageoffstage 兩個參數(shù),其中:

  • onstage 是一個 Stack缀拭,用于展示 onstageChildren.reversed.toList(growable: false) 咳短,也就是可以被看到的部分填帽;
  • offstage 是展示 offstageChildren 列表,也就是不可以被看到的部分咙好;
    return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    );

簡單些說篡腌,比如此時有 [A、B勾效、C] 三個頁面嘹悼,那么:

  • C 應該是在 onstage
  • A层宫、B 應該是處于 offstage 杨伙。

當然,A卒密、B缀台、C 都是以 OverlayEntry 的方式被插入到 Overlay 中,而 A 哮奇、B膛腐、C 頁面被插入的時候默認都是兩個 OverlayEntry ,也就是 [A鼎俘、B哲身、C] 應該有 6 個 OverlayEntry

舉個例子贸伐,程序在默認啟動之后勘天,首先看到的就是 A 頁面,這時候可以看到 Overlay

  • _entries 長度是 2捉邢,即 Overlay 中的列表總長度為2脯丝;
  • onstageChildren 長度是 2,即當前可見的 OverlayEntry 是2伏伐;
  • offstageChildren 長度是 0宠进,即沒有不可見的 OverlayEntry
image

這時候我們打開 B 頁面藐翎,可以看到 Overlay 中:

  • _entries 長度是 4材蹬,也就是 Overlay 中多插入了兩個 OverlayEntry
  • onstageChildren 長度是 4吝镣,就是當前可見的 OverlayEntry 是 4 個堤器;
  • offstageChildren 長度是 0,就是當前還沒有不可見的 OverlayEntry末贾。
image

其實這時候 Overlay 處于頁面打開中的狀態(tài)闸溃,也就是 A 頁面還可以被看到,B 頁面正在動畫打開的過程。

image

接著可以看到 Overlay 中的 build 又再次被執(zhí)行:

  • _entries 長度還是 4圈暗;
  • onstageChildren 長度變?yōu)?2掂为,即當前可見的 OverlayEntry 變成了 2 個;
  • offstageChildren 長度是 1员串,即當前有了一個不可見 OverlayEntry勇哗。
image

這時候 B 頁面其實已經(jīng)打開完畢,所以 onstageChildren 恢復為 2 的長度寸齐,也就是 B 頁面對應的那兩個 OverlayEntry欲诺;而 A 頁面不可見,所以 A 頁面被放置到了 offstageChildren渺鹦。

為什么只把 A 的一個 OverlayEntry 放到 offstageChildren扰法?這個后面會講到。

image

接著如下圖所示毅厚,再打開 C 頁面時塞颁,可以看到同樣經(jīng)歷了這個過程:

  • _entries 長度變?yōu)?6;
  • onstageChildren 長度先是 4 吸耿,之后又變成 2 祠锣,因為打開時有B 和 C 兩個頁面參與,而打開完成后只剩下一個 C 頁面咽安;
  • offstageChildren 長度是 1伴网,之后又變?yōu)?2,因為最開始只有 A 不可見妆棒,而最后 A 和 B 都不可見澡腾;
image
image

所以可以看到,每次打開一個頁面:

  • 先會向 _entries 插入兩個 OverlayEntry糕珊;
  • 之后會先經(jīng)歷 onstageChildren 長度是 4 的頁面打開過程狀態(tài)动分;
  • 最后變?yōu)?onstageChildren 長度是 2 的頁面打開完成狀態(tài),而底部的頁面由于不可見所以被加入到 offstageChildren 中红选;

2.3澜公、Overlay 和 Route

為什么每次向 _entries 插入的是兩個 OverlayEntry

這就和 Route 有關(guān)纠脾,比如默認 Navigator 打開新的頁面需要使用 MaterialPageRoute ,而生成 OverlayEntry 就是在它的基類之一的 ModalRoute 完成蜕青。

ModalRoutecreateOverlayEntries 方法中苟蹈,通過 _buildModalBarrier_buildModalScope 創(chuàng)建了兩個 OverlayEntry ,其中:

  • _buildModalBarrier 創(chuàng)建的一般是蒙層右核;
  • _buildModalScope 創(chuàng)建的 OverlayEntry 是頁面的載體慧脱;

所以默認打開一個頁面,是會存在兩個 OverlayEntry 贺喝,一個是蒙層一個是頁面菱鸥。

  @override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }

那么一個頁面有兩個 OverlayEntry 宗兼,但是為什么插入到 offstageChildren 中的數(shù)量每次都是加 1 而不是加 2?

如果單從邏輯上講氮采,按照前面 [A殷绍、B、C] 三個頁面的例子鹊漠,_entries 里有 6 個 OverlayEntry主到, 但是 B、C 頁面都不可見了躯概,把 B登钥、C 頁面的蒙層也捎帶上不就純屬浪費了?

如從代碼層面解釋娶靡,在 _entries 在倒序 for 循環(huán)的時候:

  • 在遇到 entry.opaqueture 時牧牢,后續(xù)的 OverlayEntry 就進不去 onstageChildren 中;
  • offstageChildren 中只有 entry.maintainStatetrue 才會被添加到隊列姿锭;
  @override
  Widget build(BuildContext context) {
    final List<Widget> onstageChildren = <Widget>[];
    final List<Widget> offstageChildren = <Widget>[];
    bool onstage = true;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[I];
      if (onstage) {
        onstageChildren.add(_OverlayEntry(entry));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
      }
    }
    return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    ); 
  }

而在 OverlayEntry 中:

  • opaque 表示了 OverlayEntry 是不是“阻塞”了整個 Overlay塔鳍,也就是不透明的完全覆蓋。
  • maintainState 表示這個 OverlayEntry 必須被添加到 _Theatre 中艾凯。

所以可以看到献幔,當頁面完全打開之后,在最前面的兩個 OverlayEntry

  • 蒙層 OverlayEntryopaque 會被設置為 true趾诗,這樣后面的 OverlayEntry 就不會進入到 onstageChildren蜡感,也就是不顯示;
  • 頁面 OverlayEntrymaintainState 會是 true 恃泪,這樣不可見的時候也會進入到 offstageChildren 里郑兴;
image

那么 opaque 是在哪里被設置的?

關(guān)于 opaque 的設置過程如下所示贝乎,在 MaterialPageRoute 的另一個基類 TransitionRoute 中情连,可以看到一開始蒙層的 opaque 會被設置為 false ,之后在 completed 會被設置為 opaque 览效,而 opaque 參數(shù)在 PageRoute 里就是 @override bool get opaque => true;

PopupRouteopaque 就是 false 却舀,因為 PopupRoute 一般是有透明的背景,需要和上一個頁面一起混合展示锤灿。

 void _handleStatusChanged(AnimationStatus status) {
    switch (status) {
      case AnimationStatus.completed:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
      case AnimationStatus.dismissed:
        if (!isActive) {
          navigator.finalizeRoute(this);
          assert(overlayEntries.isEmpty);
        }
        break;
    }
    changedInternalState();
  }

到這里我們就理清了頁面打開時 Overlay 的工作邏輯挽拔,默認情況下:

  • 每個頁面打開時會插入兩個 OverlayEntryOverlay
  • 打開過程中 onstageChildren 是 4 個但校,因為此時兩個頁面在混合顯示螃诅;
  • 打開完成后 onstageChildren 是 2,因為蒙層的 opaque 被設置為 ture ,后面的頁面不再是可見术裸;
  • 具備 maintainStatetrueOverlayEntry 在不可見后會進入到 offstageChildren倘是;

額外介紹下,路由被插入的位置會和 route.install 時傳入的 OverlayEntry 有關(guān)袭艺,比如: push 傳入的是 _history(頁面路由堆棧)的 last 搀崭。

三、新版 1.17 中 Overlay

那為什么在 1.17 之前匹表,打開新的頁面時舊的頁面會被執(zhí)行 build 门坷? 這里面其實主要有兩個點:

  • OverlayEntry 都有一個 GlobalKey<_OverlayEntryState> 用戶表示頁面的唯一;
  • OverlayEntry_Theatre 中會有從 onstageoffstage 的過程袍镀;

3.1默蚌、為什么會 rebuild

因為 OverlayEntryOverlay 內(nèi)部是會被轉(zhuǎn)化為 _OverlayEntry 進行工作,而 OverlayEntry 里面的 GlobalKey 自然也就用在了 _OverlayEntry 上苇羡,而當 Widget 使用了 GlobalKey绸吸,那么其對應的 Element 就會是 "Global" 的。

Element 執(zhí)行 inflateWidget 方法時设江,會判斷如果 Key 值是 GlobalKey锦茁,就會調(diào)用 _retakeInactiveElement 方法返回“已存在”的 Element 對象,從而讓 Element 被“復用”到其它位置叉存,而這個過程 Element 會從原本的 parent 那里被移除码俩,然后添加到新的 parent 上。

這個過程就會觸發(fā) Elementupdate 歼捏,而 _OverlayEntry 本身是一個 StatefulWidget 稿存,所以對應的 StatefulElementupdate 就會觸發(fā) rebuild

3.2瞳秽、為什么 1.17 不會 rebuild

那在 1.17 上瓣履,為了不出現(xiàn)每次打開頁面后還 rebuild 舊頁面的情況,這里取消了 _Theatreonstageoffstage 练俐,替換為 skipCountchildren 參數(shù)袖迎。

并且 _TheatreRenderObjectWidget 變?yōu)榱?MultiChildRenderObjectWidget,然后在 _RenderTheatre 中復用了 RenderStack 共享的布局能力腺晾。

  @override
  Widget build(BuildContext context) {
    // This list is filled backwards and then reversed below before
    // it is added to the tree.
    final List<Widget> children = <Widget>[];
    bool onstage = true;
    int onstageCount = 0;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[I];
      if (onstage) {
        onstageCount += 1;
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
        ));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
          tickerEnabled: false,
        ));
      }
    }
    return _Theatre(
      skipCount: children.length - onstageCount,
      children: children.reversed.toList(growable: false),
    );
  }

這時候等于 Overlay 中所有的 _entries 都處理到一個 MultiChildRenderObjectWidget 中燕锥,也就是同在一個 Element 中,而不是之前控件需要在 onstageStackoffstage 列表下來回切換悯蝉。

在新的 _Theatre 將兩個數(shù)組合并成一個 children 數(shù)組归形,然后將 onstageCount 之外的部分設置為 skipCount ,在布局時獲取 _firstOnstageChild 進行布局泉粉,而當 children 發(fā)生改變時连霉,觸發(fā)的是 MultiChildRenderObjectElementinsertChildRenderObject ,而不會去“干擾”到之前的頁面嗡靡,所以不會產(chǎn)生上一個頁面的 rebuild 跺撼。

  RenderBox get _firstOnstageChild {
    if (skipCount == super.childCount) {
      return null;
    }
    RenderBox child = super.firstChild;
    for (int toSkip = skipCount; toSkip > 0; toSkip--) {
      final StackParentData childParentData = child.parentData as StackParentData;
      child = childParentData.nextSibling;
      assert(child != null);
    }
    return child;
  }

  RenderBox get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;

最后如下圖所示,在打開頁面后讨彼,children 會經(jīng)歷從 4 到 3 的變化歉井,而 onstageCount 也會從 4 變?yōu)?2,也印證了頁面打開過程和關(guān)閉之后的邏輯其實并沒發(fā)生本質(zhì)的變化哈误。

image
image

從結(jié)果上看哩至,這個改動確實對性能產(chǎn)生了不錯的提升。當然蜜自,這個改進主要是在不透明的頁面之間生效菩貌,如果是透明的頁面效果比如 PopModal 之類的,那還是需要 rebuild 一下重荠。

image

四箭阶、其他優(yōu)化

Metal 是 iOS 上類似于 OpenGL ES 的底層圖形編程接口,可以在 iOS 設備上通過 api 直接操作 GPU 戈鲁。

而 1.17 開始仇参,F(xiàn)lutter 在 iOS 上對于支持 Metal 的設備將使用 Metal 進行渲染,所以官方提供的數(shù)據(jù)上看婆殿,這樣可以提高 50% 的性能诈乒。更多可見:https://github.com/flutter/flutter/wiki/Metal-on-iOS-FAQ

image

Android 上也由于 Dart VM 的優(yōu)化,體積可以下降大約 18.5% 的大小婆芦。

1.17對于加載大量圖片的處理進行了優(yōu)化怕磨,在快速滑動的過程中可以得到更好的性能提升(通過延時清理 IO Thread 的 Context),這樣理論上可以在原本基礎上節(jié)省出 70% 的內(nèi)存寞缝。

image

好了癌压,這一期想聊的聊完了,最后容我“厚顏無恥”地推廣下鄙人最近剛剛上架的新書 《Flutter 開發(fā)實戰(zhàn)詳解》荆陆,感興趣的小伙伴可以通過以下地址了解:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滩届,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子被啼,更是在濱河造成了極大的恐慌帜消,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浓体,死亡現(xiàn)場離奇詭異泡挺,居然都是意外死亡,警方通過查閱死者的電腦和手機命浴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門娄猫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贱除,“玉大人,你說我怎么就攤上這事媳溺≡禄希” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵悬蔽,是天一觀的道長扯躺。 經(jīng)常有香客問我,道長蝎困,這世上最難降的妖魔是什么录语? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮禾乘,結(jié)果婚禮上澎埠,老公的妹妹穿的比我還像新娘。我一直安慰自己始藕,他們只是感情好失暂,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鳄虱,像睡著了一般弟塞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拙已,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天决记,我揣著相機與錄音,去河邊找鬼倍踪。 笑死系宫,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的建车。 我是一名探鬼主播扩借,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缤至!你這毒婦竟也來了潮罪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤领斥,失蹤者是張志新(化名)和其女友劉穎嫉到,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體月洛,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡何恶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嚼黔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片细层。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡惜辑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疫赎,到底是詐尸還是另有隱情韵丑,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布虚缎,位于F島的核電站,受9級特大地震影響钓株,放射性物質(zhì)發(fā)生泄漏实牡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一轴合、第九天 我趴在偏房一處隱蔽的房頂上張望创坞。 院中可真熱鬧,春花似錦受葛、人聲如沸题涨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纲堵。三九已至,卻和暖如春闰渔,著一層夾襖步出監(jiān)牢的瞬間席函,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工冈涧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留茂附,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓督弓,卻偏偏與公主長得像营曼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子愚隧,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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