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.dart 和 overlay.dart 兩個(gè)文件上。
stack.dart文件的修改,只是為了將RenderStack的相關(guān)邏輯變?yōu)楣蚕淼撵o態(tài)方法getIntrinsicDimension和layoutPositionedChild,其實(shí)就是共享Stack的部分布局能力給Overlay。overlay.dart文件的修改則是這次的靈魂所在。
二、Navigator 的 Overlay
事實(shí)上我們常用的 Navigator 是一個(gè) StatefulWidget, 而常用的 pop 、push 等方法對(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è)圖層,而OverlayEntry 的 builder 方法會(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 中有 onstage 和 offstage 兩個(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;
這時(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。
其實(shí)這時(shí)候 Overlay 處于頁(yè)面打開中的狀態(tài),也就是 A 頁(yè)面還可以被看到,B 頁(yè)面正在動(dòng)畫打開的過程。
接著可以看到 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。
這時(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ì)講到。
接著如下圖所示,再打開 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 都不可見;
所以可以看到,每次打開一個(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 完成。
在 ModalRoute 的 createOverlayEntries 方法中,通過 _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.opaque為ture時(shí),后續(xù)的OverlayEntry就進(jìn)不去onstageChildren中; -
offstageChildren中只有entry.maintainState為true才會(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:
- 蒙層
OverlayEntry的opaque會(huì)被設(shè)置為 true,這樣后面的OverlayEntry就不會(huì)進(jìn)入到onstageChildren,也就是不顯示; - 頁(yè)面
OverlayEntry的maintainState會(huì)是true,這樣不可見的時(shí)候也會(huì)進(jìn)入到offstageChildren里;
那么 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;
在
PopupRoute中opaque就是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è)
OverlayEntry到Overlay; - 打開過程中
onstageChildren是 4 個(gè),因?yàn)榇藭r(shí)兩個(gè)頁(yè)面在混合顯示; - 打開完成后
onstageChildren是 2,因?yàn)槊蓪拥?opaque被設(shè)置為ture,后面的頁(yè)面不再是可見; - 具備
maintainState為true的OverlayEntry在不可見后會(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ì)有從onstage到offstage的過程;
3.1、為什么會(huì) rebuild
因?yàn)?OverlayEntry 在 Overlay 內(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ā) Element 的 update ,而 _OverlayEntry 本身是一個(gè) StatefulWidget ,所以對(duì)應(yīng)的 StatefulElement 的 update 就會(huì)觸發(fā) rebuild 。
3.2、為什么 1.17 不會(huì) rebuild
那在 1.17 上,為了不出現(xiàn)每次打開頁(yè)面后還 rebuild 舊頁(yè)面的情況,這里取消了 _Theatre 的 onstage 和 offstage ,替換為 skipCount 和 children 參數(shù)。
并且 _Theatre 從 RenderObjectWidget 變?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 中,而不是之前控件需要在 onstage 的 Stack 和 offstage 列表下來回切換。
在新的 _Theatre 將兩個(gè)數(shù)組合并成一個(gè) children 數(shù)組,然后將 onstageCount 之外的部分設(shè)置為 skipCount ,在布局時(shí)獲取 _firstOnstageChild 進(jìn)行布局,而當(dāng) children 發(fā)生改變時(shí),觸發(fā)的是 MultiChildRenderObjectElement 的 insertChildRenderObject ,而不會(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ì)的變化。
從結(jié)果上看,這個(gè)改動(dòng)確實(shí)對(duì)性能產(chǎn)生了不錯(cuò)的提升。當(dāng)然,這個(gè)改進(jìn)主要是在不透明的頁(yè)面之間生效,如果是透明的頁(yè)面效果比如 PopModal 之類的,那還是需要 rebuild 一下。
四、其他優(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
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)存。
好了,這一期想聊的聊完了,最后容我“厚顏無恥”地推廣下鄙人最近剛剛上架的新書 《Flutter 開發(fā)實(shí)戰(zhàn)詳解》,感興趣的小伙伴可以通過以下地址了解:
當(dāng)當(dāng):http://product.dangdang.com/28558519.html