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

Flutter 1.17 對比上一個穩定版本,更可能是帶來了性能上的提高,其中一個關鍵的優化點就是 Navigator 的內部邏輯,本篇將帶你解密 Navigator 從 1.12 到 1.17 的變化,並介紹 Flutter 1.17 上究竟優化了哪些性能。html

1、Navigator 優化了什麼?

在 1.17 版本最讓人感興趣的變更莫過於:「打開新的不透明頁面以後,路由裏的舊頁面不會再觸發 buildgit

雖然以前介紹過 build 方法自己很輕,可是在「不須要」的時候「不執行」明顯更符合咱們的預期,而這個優化的 PR 主要體如今 stack.dartoverlay.dart 兩個文件上。github

  • stack.dart 文件的修改,只是爲了將 RenderStack 的相關邏輯變爲共享的靜態方法 getIntrinsicDimensionlayoutPositionedChild ,其實就是共享 Stack 的部分佈局能力給 Overlay編程

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

2、Navigator 的 Overlay

事實上咱們經常使用的 Navigator 是一個 StatefulWidget, 而經常使用的 poppush 等方法對應的邏輯都是在 NavigatorState 中,而 NavigatorState 主要是經過 Overlay 來承載路由頁面,因此導航頁面間的管理邏輯主要在於 Overlay數組

2.一、Overlay 是什麼?

Overlay 你們可能用過,在 Flutter 中能夠經過 Overlay 來向 MaterialApp 添加全局懸浮控件,這是由於Overlay 是一個相似 Stack 層級控件,可是它能夠經過 OverlayEntry 來獨立地管理內部控件的展現。bash

好比能夠經過 overlayState.insert 插入一個 OverlayEntry 來實現插入一個圖層,而OverlayEntrybuilder 方法會在展現時被調用,從而出現須要的佈局效果。ide

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.二、Overlay 如何實現導航?

Navigator 中其實也是使用了 Overlay 實現頁面管理,每一個打開的 Route 默認狀況下是向 Overlay 插入了兩個 OverlayEntry佈局

爲何是兩個後面會介紹。性能

而在 Overlay 中, List<OverlayEntry> _entries 的展現邏輯又是經過 _Theatre 來完成的,在 _Theatre 中有 onstageoffstage 兩個參數,其中:

  • 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

這時候咱們打開 B 頁面,能夠看到 Overlay 中:

  • _entries 長度是 4,也就是 Overlay 中多插入了兩個 OverlayEntry
  • onstageChildren 長度是 4,就是當前可見的 OverlayEntry 是 4 個;
  • offstageChildren 長度是 0,就是當前尚未不可見的 OverlayEntry

其實這時候 Overlay 處於頁面打開中的狀態,也就是 A 頁面還能夠被看到,B 頁面正在動畫打開的過程。

接着能夠看到 Overlay 中的 build 又再次被執行:

  • _entries 長度仍是 4;
  • onstageChildren 長度變爲 2,即當前可見的 OverlayEntry 變成了 2 個;
  • offstageChildren 長度是 1,即當前有了一個不可見 OverlayEntry

這時候 B 頁面其實已經打開完畢,因此 onstageChildren 恢復爲 2 的長度,也就是 B 頁面對應的那兩個 OverlayEntry;而 A 頁面不可見,因此 A 頁面被放置到了 offstageChildren

爲何只把 A 的一個 OverlayEntry 放到 offstageChildren?這個後面會講到。

接着以下圖所示,再打開 C 頁面時,能夠看到一樣經歷了這個過程:

  • _entries 長度變爲 6;
  • onstageChildren 長度先是 4 ,以後又變成 2 ,由於打開時有B 和 C 兩個頁面參與,而打開完成後只剩下一個 C 頁面;
  • offstageChildren 長度是 1,以後又變爲 2,由於最開始只有 A 不可見,而最後 A 和 B 都不可見;

因此能夠看到,每次打開一個頁面:

  • 先會向 _entries 插入兩個 OverlayEntry
  • 以後會先經歷 onstageChildren 長度是 4 的頁面打開過程狀態;
  • 最後變爲 onstageChildren 長度是 2 的頁面打開完成狀態,而底部的頁面因爲不可見因此被加入到 offstageChildren 中;

2.三、Overlay 和 Route

爲何每次向 _entries 插入的是兩個 OverlayEntry

這就和 Route 有關,好比默認 Navigator 打開新的頁面須要使用 MaterialPageRoute ,而生成 OverlayEntry 就是在它的基類之一的 ModalRoute 完成。

ModalRoutecreateOverlayEntries 方法中,經過 _buildModalBarrier_buildModalScope 建立了兩個 OverlayEntry ,其中:

  • _buildModalBarrier 建立的通常是蒙層;
  • _buildModalScope 建立的 OverlayEntry 是頁面的載體;

因此默認打開一個頁面,是會存在兩個 OverlayEntry ,一個是蒙層一個是頁面

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

那麼一個頁面有兩個 OverlayEntry ,可是爲何插入到 offstageChildren 中的數量每次都是加 1 而不是加 2?

若是單從邏輯上講,按照前面 [A、B、C] 三個頁面的例子,_entries 裏有 6 個 OverlayEntry, 可是 B、C 頁面都不可見了,把 B、C 頁面的蒙層也捎帶上不就純屬浪費了?

如從代碼層面解釋,在 _entries 在倒序 for 循環的時候:

  • 在遇到 entry.opaqueture 時,後續的 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 裏;

那麼 opaque 是在哪裏被設置的?

關於 opaque 的設置過程以下所示,在 MaterialPageRoute 的另外一個基類 TransitionRoute 中,能夠看到一開始蒙層的 opaque 會被設置爲 false ,以後在 completed 會被設置爲 opaque ,而 opaque 參數在 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 有關,好比: push 傳入的是 _history(頁面路由堆棧)的 last 。

3、新版 1.17 中 Overlay

那爲何在 1.17 以前,打開新的頁面時舊的頁面會被執行 build 這裏面其實主要有兩個點:

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

3.一、爲何會 rebuild

由於 OverlayEntryOverlay 內部是會被轉化爲 _OverlayEntry 進行工做,而 OverlayEntry 裏面的 GlobalKey 天然也就用在了 _OverlayEntry 上,而當 Widget 使用了 GlobalKey,那麼其對應的 Element 就會是 "Global" 的。

Element 執行 inflateWidget 方法時,會判斷若是 Key 值是 GlobalKey,就會調用 _retakeInactiveElement 方法返回「已存在」的 Element 對象,從而讓 Element 被「複用」到其它位置,而這個過程 Element 會從本來的 parent 那裏被移除,而後添加到新的 parent 上。

這個過程就會觸發 Elementupdate ,而 _OverlayEntry 自己是一個 StatefulWidget ,因此對應的 StatefulElementupdate 就會觸發 rebuild

3.二、爲何 1.17 不會 rebuild

那在 1.17 上,爲了避免出現每次打開頁面後還 rebuild 舊頁面的狀況,這裏取消了 _Theatreonstageoffstage ,替換爲 skipCountchildren 參數。

而且 _TheatreRenderObjectWidget 變爲了 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 將兩個數組合併成一個 children 數組,而後將 onstageCount 以外的部分設置爲 skipCount ,在佈局時獲取 _firstOnstageChild 進行佈局,而當 children 發生改變時,觸發的是 MultiChildRenderObjectElementinsertChildRenderObject ,而不會去「干擾」到以前的頁面,因此不會產生上一個頁面的 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 會經歷從 4 到 3 的變化,而 onstageCount 也會從 4 變爲 2,也印證了頁面打開過程和關閉以後的邏輯其實並沒發生本質的變化。

從結果上看,這個改動確實對性能產生了不錯的提高。固然,這個改進主要是在不透明的頁面之間生效,若是是透明的頁面效果好比 PopModal 之類的,那仍是須要 rebuild 一下。

4、其餘優化

Metal 是 iOS 上相似於 OpenGL ES 的底層圖形編程接口,能夠在 iOS 設備上經過 api 直接操做 GPU 。

而 1.17 開始,Flutter 在 iOS 上對於支持 Metal 的設備將使用 Metal 進行渲染,因此官方提供的數據上看,這樣能夠提升 50% 的性能。更多可見:github.com/flutter/flu…

Android 上也因爲 Dart VM 的優化,體積能夠降低大約 18.5% 的大小。

1.17對於加載大量圖片的處理進行了優化,在快速滑動的過程當中能夠獲得更好的性能提高(經過延時清理 IO Thread 的 Context),這樣理論上能夠在本來基礎上節省出 70% 的內存。

好了,這一期想聊的聊完了,最後容我「厚顏無恥」地推廣下鄙人最近剛剛上架的新書 《Flutter 開發實戰詳解》,感興趣的小夥伴能夠經過如下地址瞭解:

相關文章
相關標籤/搜索