Flutter 1.17 對比上一個穩定版本,更可能是帶來了性能上的提高,其中一個關鍵的優化點就是 Navigator
的內部邏輯,本篇將帶你解密 Navigator
從 1.12 到 1.17 的變化,並介紹 Flutter 1.17 上究竟優化了哪些性能。html
在 1.17 版本最讓人感興趣的變更莫過於:「打開新的不透明頁面以後,路由裏的舊頁面不會再觸發 build
」。git
雖然以前介紹過 build
方法自己很輕,可是在「不須要」的時候「不執行」明顯更符合咱們的預期,而這個優化的 PR 主要體如今 stack.dart
和 overlay.dart
兩個文件上。github
stack.dart
文件的修改,只是爲了將 RenderStack
的相關邏輯變爲共享的靜態方法 getIntrinsicDimension
和 layoutPositionedChild
,其實就是共享 Stack
的部分佈局能力給 Overlay
。編程
overlay.dart
文件的修改則是此次的靈魂所在。api
事實上咱們經常使用的 Navigator
是一個 StatefulWidget
, 而經常使用的 pop
、push
等方法對應的邏輯都是在 NavigatorState
中,而 NavigatorState
主要是經過 Overlay
來承載路由頁面,因此導航頁面間的管理邏輯主要在於 Overlay
。數組
Overlay
你們可能用過,在 Flutter 中能夠經過 Overlay
來向 MaterialApp
添加全局懸浮控件,這是由於Overlay
是一個相似 Stack
層級控件,可是它能夠經過 OverlayEntry
來獨立地管理內部控件的展現。bash
好比能夠經過 overlayState.insert
插入一個 OverlayEntry
來實現插入一個圖層,而OverlayEntry
的 builder
方法會在展現時被調用,從而出現須要的佈局效果。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);
複製代碼
在 Navigator
中其實也是使用了 Overlay
實現頁面管理,每一個打開的 Route
默認狀況下是向 Overlay
插入了兩個 OverlayEntry
。佈局
爲何是兩個後面會介紹。性能
而在 Overlay
中, List<OverlayEntry> _entries
的展現邏輯又是經過 _Theatre
來完成的,在 _Theatre
中有 onstage
和 offstage
兩個參數,其中:
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] 三個頁面,那麼:
onstage
;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
中;爲何每次向 _entries
插入的是兩個 OverlayEntry
?
這就和 Route
有關,好比默認 Navigator
打開新的頁面須要使用 MaterialPageRoute
,而生成 OverlayEntry
就是在它的基類之一的 ModalRoute
完成。
在 ModalRoute
的 createOverlayEntries
方法中,經過 _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.opaque
爲 ture
時,後續的 OverlayEntry
就進不去 onstageChildren
中;offstageChildren
中只有 entry.maintainState
爲 true
纔會被添加到隊列;@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
:
OverlayEntry
的 opaque
會被設置爲 true,這樣後面的 OverlayEntry
就不會進入到 onstageChildren
,也就是不顯示;OverlayEntry
的 maintainState
會是 true
,這樣不可見的時候也會進入到 offstageChildren
裏;那麼 opaque
是在哪裏被設置的?
關於 opaque
的設置過程以下所示,在 MaterialPageRoute
的另外一個基類 TransitionRoute
中,能夠看到一開始蒙層的 opaque
會被設置爲 false
,以後在 completed
會被設置爲 opaque
,而 opaque
參數在 PageRoute
裏就是 @override bool get opaque => true;
在
PopupRoute
中opaque
就是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
的工做邏輯,默認狀況下:
OverlayEntry
到 Overlay
;onstageChildren
是 4 個,由於此時兩個頁面在混合顯示;onstageChildren
是 2,由於蒙層的 opaque
被設置爲 ture
,後面的頁面再也不是可見;maintainState
爲 true
的 OverlayEntry
在不可見後會進入到 offstageChildren
;額外介紹下,路由被插入的位置會和
route.install
時傳入的OverlayEntry
有關,好比:push
傳入的是_history
(頁面路由堆棧)的 last 。
那爲何在 1.17 以前,打開新的頁面時舊的頁面會被執行 build
? 這裏面其實主要有兩個點:
OverlayEntry
都有一個 GlobalKey<_OverlayEntryState>
用戶表示頁面的惟一;OverlayEntry
在 _Theatre
中會有從 onstage
到 offstage
的過程;由於 OverlayEntry
在 Overlay
內部是會被轉化爲 _OverlayEntry
進行工做,而 OverlayEntry
裏面的 GlobalKey
天然也就用在了 _OverlayEntry
上,而當 Widget
使用了 GlobalKey
,那麼其對應的 Element
就會是 "Global" 的。
在 Element
執行 inflateWidget
方法時,會判斷若是 Key
值是 GlobalKey
,就會調用 _retakeInactiveElement
方法返回「已存在」的 Element
對象,從而讓 Element
被「複用」到其它位置,而這個過程 Element
會從本來的 parent
那裏被移除,而後添加到新的 parent
上。
這個過程就會觸發 Element
的 update
,而 _OverlayEntry
自己是一個 StatefulWidget
,因此對應的 StatefulElement
的 update
就會觸發 rebuild
。
那在 1.17 上,爲了避免出現每次打開頁面後還 rebuild
舊頁面的狀況,這裏取消了 _Theatre
的 onstage
和 offstage
,替換爲 skipCount
和 children
參數。
而且 _Theatre
從 RenderObjectWidget
變爲了 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
中,而不是以前控件須要在 onstage
的 Stack
和 offstage
列表下來回切換。
在新的 _Theatre
將兩個數組合併成一個 children
數組,而後將 onstageCount
以外的部分設置爲 skipCount
,在佈局時獲取 _firstOnstageChild
進行佈局,而當 children
發生改變時,觸發的是 MultiChildRenderObjectElement
的 insertChildRenderObject
,而不會去「干擾」到以前的頁面,因此不會產生上一個頁面的 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
一下。
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 開發實戰詳解》,感興趣的小夥伴能夠經過如下地址瞭解: