Flutter 1.17 中的導(dǎo)航解密和性能提升

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

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

在 1.17 版本最讓人感興趣的變動(dòng)莫過于:“打開新的不透明頁(yè)面之后,路由里的舊頁(yè)面不會(huì)再觸發(fā) build。

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

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

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

二、Navigator 的 Overlay

事實(shí)上我們常用的 Navigator 是一個(gè) StatefulWidget, 而常用的 poppush 等方法對(duì)應(yīng)的邏輯都是在 NavigatorState 中,而 NavigatorState 主要是通過 Overlay 來承載路由頁(yè)面,所以導(dǎo)航頁(yè)面間的管理邏輯主要在于 Overlay。

2.1、Overlay 是什么?

Overlay 大家可能用過,在 Flutter 中可以通過 Overlay 來向 MaterialApp 添加全局懸浮控件,這是因?yàn)?code>Overlay 是一個(gè)類似 Stack 層級(jí)控件,但是它可以通過 OverlayEntry 來獨(dú)立地管理內(nèi)部控件的展示。

比如可以通過 overlayState.insert 插入一個(gè) OverlayEntry 來實(shí)現(xiàn)插入一個(gè)圖層,而OverlayEntrybuilder 方法會(huì)在展示時(shí)被調(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 如何實(shí)現(xiàn)導(dǎo)航?

Navigator 中其實(shí)也是使用了 Overlay 實(shí)現(xiàn)頁(yè)面管理,每個(gè)打開的 Route 默認(rèn)情況下是向 Overlay 插入了兩個(gè) OverlayEntry

為什么是兩個(gè)后面會(huì)介紹。

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

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

簡(jiǎn)單些說,比如此時(shí)有 [A、B、C] 三個(gè)頁(yè)面,那么:

  • C 應(yīng)該是在 onstage ;
  • A、B 應(yīng)該是處于 offstage 。

當(dāng)然,A、B、C 都是以 OverlayEntry 的方式被插入到 Overlay 中,而 A 、B、C 頁(yè)面被插入的時(shí)候默認(rèn)都是兩個(gè) OverlayEntry ,也就是 [A、B、C] 應(yīng)該有 6 個(gè) OverlayEntry

舉個(gè)例子,程序在默認(rèn)啟動(dòng)之后,首先看到的就是 A 頁(yè)面,這時(shí)候可以看到 Overlay

  • _entries 長(zhǎng)度是 2,即 Overlay 中的列表總長(zhǎng)度為2;
  • onstageChildren 長(zhǎng)度是 2,即當(dāng)前可見的 OverlayEntry 是2;
  • offstageChildren 長(zhǎng)度是 0,即沒有不可見的 OverlayEntry
image

這時(shí)候我們打開 B 頁(yè)面,可以看到 Overlay 中:

  • _entries 長(zhǎng)度是 4,也就是 Overlay 中多插入了兩個(gè) OverlayEntry
  • onstageChildren 長(zhǎng)度是 4,就是當(dāng)前可見的 OverlayEntry 是 4 個(gè);
  • offstageChildren 長(zhǎng)度是 0,就是當(dāng)前還沒有不可見的 OverlayEntry。
image

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

image

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

  • _entries 長(zhǎng)度還是 4;
  • onstageChildren 長(zhǎng)度變?yōu)?2,即當(dāng)前可見的 OverlayEntry 變成了 2 個(gè);
  • offstageChildren 長(zhǎng)度是 1,即當(dāng)前有了一個(gè)不可見 OverlayEntry
image

這時(shí)候 B 頁(yè)面其實(shí)已經(jīng)打開完畢,所以 onstageChildren 恢復(fù)為 2 的長(zhǎng)度,也就是 B 頁(yè)面對(duì)應(yīng)的那兩個(gè) OverlayEntry;而 A 頁(yè)面不可見,所以 A 頁(yè)面被放置到了 offstageChildren

為什么只把 A 的一個(gè) OverlayEntry 放到 offstageChildren?這個(gè)后面會(huì)講到。

image

接著如下圖所示,再打開 C 頁(yè)面時(shí),可以看到同樣經(jīng)歷了這個(gè)過程:

  • _entries 長(zhǎng)度變?yōu)?6;
  • onstageChildren 長(zhǎng)度先是 4 ,之后又變成 2 ,因?yàn)榇蜷_時(shí)有B 和 C 兩個(gè)頁(yè)面參與,而打開完成后只剩下一個(gè) C 頁(yè)面;
  • offstageChildren 長(zhǎng)度是 1,之后又變?yōu)?2,因?yàn)樽铋_始只有 A 不可見,而最后 A 和 B 都不可見;
image
image

所以可以看到,每次打開一個(gè)頁(yè)面:

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

2.3、Overlay 和 Route

為什么每次向 _entries 插入的是兩個(gè) OverlayEntry

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

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

  • _buildModalBarrier 創(chuàng)建的一般是蒙層;
  • _buildModalScope 創(chuàng)建的 OverlayEntry 是頁(yè)面的載體;

所以默認(rèn)打開一個(gè)頁(yè)面,是會(huì)存在兩個(gè) OverlayEntry ,一個(gè)是蒙層一個(gè)是頁(yè)面。

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

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

如果單從邏輯上講,按照前面 [A、B、C] 三個(gè)頁(yè)面的例子,_entries 里有 6 個(gè) OverlayEntry, 但是 B、C 頁(yè)面都不可見了,把 B、C 頁(yè)面的蒙層也捎帶上不就純屬浪費(fèi)了?

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

  • 在遇到 entry.opaqueture 時(shí),后續(xù)的 OverlayEntry 就進(jìn)不去 onstageChildren 中;
  • offstageChildren 中只有 entry.maintainStatetrue 才會(huì)被添加到隊(duì)列;
  @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 是不是“阻塞”了整個(gè) Overlay,也就是不透明的完全覆蓋。
  • maintainState 表示這個(gè) OverlayEntry 必須被添加到 _Theatre 中。

所以可以看到,當(dāng)頁(yè)面完全打開之后,在最前面的兩個(gè) OverlayEntry

  • 蒙層 OverlayEntryopaque 會(huì)被設(shè)置為 true,這樣后面的 OverlayEntry 就不會(huì)進(jìn)入到 onstageChildren,也就是不顯示;
  • 頁(yè)面 OverlayEntrymaintainState 會(huì)是 true ,這樣不可見的時(shí)候也會(huì)進(jìn)入到 offstageChildren 里;
image

那么 opaque 是在哪里被設(shè)置的?

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

PopupRouteopaque 就是 false ,因?yàn)?PopupRoute 一般是有透明的背景,需要和上一個(gè)頁(yè)面一起混合展示。

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

到這里我們就理清了頁(yè)面打開時(shí) Overlay 的工作邏輯,默認(rèn)情況下:

  • 每個(gè)頁(yè)面打開時(shí)會(huì)插入兩個(gè) OverlayEntryOverlay
  • 打開過程中 onstageChildren 是 4 個(gè),因?yàn)榇藭r(shí)兩個(gè)頁(yè)面在混合顯示;
  • 打開完成后 onstageChildren 是 2,因?yàn)槊蓪拥?opaque 被設(shè)置為 ture ,后面的頁(yè)面不再是可見;
  • 具備 maintainStatetrueOverlayEntry 在不可見后會(huì)進(jìn)入到 offstageChildren

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

三、新版 1.17 中 Overlay

那為什么在 1.17 之前,打開新的頁(yè)面時(shí)舊的頁(yè)面會(huì)被執(zhí)行 build 這里面其實(shí)主要有兩個(gè)點(diǎn):

  • OverlayEntry 都有一個(gè) GlobalKey<_OverlayEntryState> 用戶表示頁(yè)面的唯一;
  • OverlayEntry_Theatre 中會(huì)有從 onstageoffstage 的過程;

3.1、為什么會(huì) rebuild

因?yàn)?OverlayEntryOverlay 內(nèi)部是會(huì)被轉(zhuǎn)化為 _OverlayEntry 進(jìn)行工作,而 OverlayEntry 里面的 GlobalKey 自然也就用在了 _OverlayEntry 上,而當(dāng) Widget 使用了 GlobalKey,那么其對(duì)應(yīng)的 Element 就會(huì)是 "Global" 的。

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

這個(gè)過程就會(huì)觸發(fā) Elementupdate ,而 _OverlayEntry 本身是一個(gè) StatefulWidget ,所以對(duì)應(yīng)的 StatefulElementupdate 就會(huì)觸發(fā) rebuild 。

3.2、為什么 1.17 不會(huì) rebuild

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

并且 _TheatreRenderObjectWidget 變?yōu)榱?MultiChildRenderObjectWidget,然后在 _RenderTheatre 中復(fù)用了 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),
    );
  }

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

在新的 _Theatre 將兩個(gè)數(shù)組合并成一個(gè) children 數(shù)組,然后將 onstageCount 之外的部分設(shè)置為 skipCount ,在布局時(shí)獲取 _firstOnstageChild 進(jìn)行布局,而當(dāng) children 發(fā)生改變時(shí),觸發(fā)的是 MultiChildRenderObjectElementinsertChildRenderObject ,而不會(huì)去“干擾”到之前的頁(yè)面,所以不會(huì)產(chǎn)生上一個(gè)頁(yè)面的 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;

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

image
image

從結(jié)果上看,這個(gè)改動(dòng)確實(shí)對(duì)性能產(chǎn)生了不錯(cuò)的提升。當(dāng)然,這個(gè)改進(jìn)主要是在不透明的頁(yè)面之間生效,如果是透明的頁(yè)面效果比如 PopModal 之類的,那還是需要 rebuild 一下。

image

四、其他優(yōu)化

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

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

image

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

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

image

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容