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.dart
和 overlay.dart
兩個文件上兔毙。
stack.dart
文件的修改,只是為了將RenderStack
的相關(guān)邏輯變?yōu)楣蚕淼撵o態(tài)方法getIntrinsicDimension
和layoutPositionedChild
兄春,其實就是共享Stack
的部分布局能力給Overlay
澎剥。overlay.dart
文件的修改則是這次的靈魂所在。
二赶舆、Navigator 的 Overlay
事實上我們常用的 Navigator
是一個 StatefulWidget
哑姚, 而常用的 pop
、push
等方法對應的邏輯都是在 NavigatorState
中芜茵,而 NavigatorState
主要是通過 Overlay
來承載路由頁面叙量,所以導航頁面間的管理邏輯主要在于 Overlay
。
2.1九串、Overlay 是什么宛乃?
Overlay
大家可能用過,在 Flutter 中可以通過 Overlay
來向 MaterialApp
添加全局懸浮控件蒸辆,這是因為Overlay
是一個類似 Stack
層級控件,但是它可以通過 OverlayEntry
來獨立地管理內(nèi)部控件的展示析既。
比如可以通過 overlayState.insert
插入一個 OverlayEntry
來實現(xiàn)插入一個圖層躬贡,而OverlayEntry
的 builder
方法會在展示時被調(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
中有 onstage
和 offstage
兩個參數(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
;
這時候我們打開 B 頁面藐翎,可以看到 Overlay
中:
-
_entries
長度是 4材蹬,也就是Overlay
中多插入了兩個OverlayEntry
; -
onstageChildren
長度是 4吝镣,就是當前可見的OverlayEntry
是 4 個堤器; -
offstageChildren
長度是 0,就是當前還沒有不可見的OverlayEntry
末贾。
其實這時候 Overlay
處于頁面打開中的狀態(tài)闸溃,也就是 A 頁面還可以被看到,B 頁面正在動畫打開的過程。
接著可以看到 Overlay
中的 build
又再次被執(zhí)行:
-
_entries
長度還是 4圈暗; -
onstageChildren
長度變?yōu)?2掂为,即當前可見的OverlayEntry
變成了 2 個; -
offstageChildren
長度是 1员串,即當前有了一個不可見OverlayEntry
勇哗。
這時候 B 頁面其實已經(jīng)打開完畢,所以 onstageChildren
恢復為 2 的長度寸齐,也就是 B 頁面對應的那兩個 OverlayEntry
欲诺;而 A 頁面不可見,所以 A 頁面被放置到了 offstageChildren
渺鹦。
為什么只把 A 的一個
OverlayEntry
放到offstageChildren
扰法?這個后面會講到。
接著如下圖所示毅厚,再打開 C 頁面時塞颁,可以看到同樣經(jīng)歷了這個過程:
-
_entries
長度變?yōu)?6; -
onstageChildren
長度先是 4 吸耿,之后又變成 2 祠锣,因為打開時有B 和 C 兩個頁面參與,而打開完成后只剩下一個 C 頁面咽安; -
offstageChildren
長度是 1伴网,之后又變?yōu)?2,因為最開始只有 A 不可見妆棒,而最后 A 和 B 都不可見澡腾;
所以可以看到,每次打開一個頁面:
- 先會向
_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
完成蜕青。
在 ModalRoute
的 createOverlayEntries
方法中苟蹈,通過 _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.opaque
為ture
時牧牢,后續(xù)的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
是在哪里被設置的?
關(guān)于 opaque
的設置過程如下所示贝乎,在 MaterialPageRoute
的另一個基類 TransitionRoute
中情连,可以看到一開始蒙層的 opaque
會被設置為 false
,之后在 completed
會被設置為 opaque
览效,而 opaque
參數(shù)在 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
有關(guān)袭艺,比如:push
傳入的是_history
(頁面路由堆棧)的 last 搀崭。
三、新版 1.17 中 Overlay
那為什么在 1.17 之前匹表,打開新的頁面時舊的頁面會被執(zhí)行 build
门坷? 這里面其實主要有兩個點:
-
OverlayEntry
都有一個GlobalKey<_OverlayEntryState>
用戶表示頁面的唯一; -
OverlayEntry
在_Theatre
中會有從onstage
到offstage
的過程袍镀;
3.1默蚌、為什么會 rebuild
因為 OverlayEntry
在 Overlay
內(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ā) Element
的 update
歼捏,而 _OverlayEntry
本身是一個 StatefulWidget
稿存,所以對應的 StatefulElement
的 update
就會觸發(fā) rebuild
。
3.2瞳秽、為什么 1.17 不會 rebuild
那在 1.17 上瓣履,為了不出現(xiàn)每次打開頁面后還 rebuild
舊頁面的情況,這里取消了 _Theatre
的 onstage
和 offstage
练俐,替換為 skipCount
和 children
參數(shù)袖迎。
并且 _Theatre
從 RenderObjectWidget
變?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
中,而不是之前控件需要在 onstage
的 Stack
和 offstage
列表下來回切換悯蝉。
在新的 _Theatre
將兩個數(shù)組合并成一個 children
數(shù)組归形,然后將 onstageCount
之外的部分設置為 skipCount
,在布局時獲取 _firstOnstageChild
進行布局泉粉,而當 children
發(fā)生改變時连霉,觸發(fā)的是 MultiChildRenderObjectElement
的 insertChildRenderObject
,而不會去“干擾”到之前的頁面嗡靡,所以不會產(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ì)的變化哈误。
從結(jié)果上看哩至,這個改動確實對性能產(chǎn)生了不錯的提升。當然蜜自,這個改進主要是在不透明的頁面之間生效菩貌,如果是透明的頁面效果比如 PopModal
之類的,那還是需要 rebuild
一下重荠。
四箭阶、其他優(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
Android 上也由于 Dart VM 的優(yōu)化,體積可以下降大約 18.5% 的大小婆芦。
1.17對于加載大量圖片的處理進行了優(yōu)化怕磨,在快速滑動的過程中可以得到更好的性能提升(通過延時清理 IO Thread 的 Context),這樣理論上可以在原本基礎上節(jié)省出 70% 的內(nèi)存寞缝。
好了癌压,這一期想聊的聊完了,最后容我“厚顏無恥”地推廣下鄙人最近剛剛上架的新書 《Flutter 開發(fā)實戰(zhàn)詳解》荆陆,感興趣的小伙伴可以通過以下地址了解: