發(fā)布時(shí)間:2023-11-27 21:31:22 瀏覽量:111次
作者:airingdeng,騰訊QQ前端開發(fā)工程師
本文將從 Flutter 原理出發(fā),詳細(xì)介紹 Flutter 的繪制原理,借由此來對(duì)比三種跨端方案;之后再進(jìn)入第三篇章 Flutter 混合開發(fā)模式的講解,主要是四種不同的 Flutter 混合模式的原理分析;最后簡單分享一下混合工程的工程化探索。
目錄:
“唯有深入,方能淺出”,對(duì)于一門技術(shù),只有了解的深入,才能用最淺顯、通俗的話語描述出。在此之前,我寫過一些 Flutter 的文章,但性質(zhì)更偏向于學(xué)習(xí)筆記與源碼閱讀筆記,因此較為晦澀,且零碎繁亂。本文作為階段性的總結(jié),我盡可能以淺顯易懂的文字、循序漸進(jìn)地來分享 Flutter 混合開發(fā)的知識(shí),對(duì)于關(guān)鍵內(nèi)容會(huì)輔以源碼或源碼中的關(guān)鍵函數(shù)來解讀,但不會(huì)成段粘貼源碼。源碼學(xué)習(xí)的效果主要在于自身,所以若對(duì)源碼學(xué)習(xí)感興趣的,可以自行閱讀 Framework 與 Engine 的源碼,也可以閱讀我過往的幾篇文章。
好了,那廢話不多說,直接開始吧!
傳統(tǒng)慣例,只要說到 Flutter 原理的文章,在開頭都會(huì)擺上這張圖。不論講的好不好,都是先擺出來,然后大部分還是靠自行領(lǐng)悟。因?yàn)檫@張圖實(shí)在太好用了。
擺出這張圖,還是簡單從整體上來先認(rèn)識(shí)了一下什么是 Flutter,否則容易陷入“盲人摸象”的境地。
Flutter 架構(gòu)采用分層設(shè)計(jì),從下到上分為三層,依次為:Embedder、Engine、Framework。
至于更多詳情,這張圖配合源碼食用體驗(yàn)會(huì)更好。但由于本文不是源碼解析,所以這個(gè)工作本文就不展開了。接下來,我會(huì)以 Flutter 繪制流程為例,來講解 Flutter 是如何工作的。這也能更好地幫助你理解源碼的思路。
Flutter 繪制流程總結(jié)了一下大體上如下圖所示:
首先是用戶操作,觸發(fā) Widget Tree 的更新,然后構(gòu)建 Element Tree,計(jì)算重繪區(qū)后將信息同步給 RenderObject Tree,之后實(shí)現(xiàn)組件布局、組件繪制、圖層合成、引擎渲染。
作為前置知識(shí),我們先來看看渲染過程中涉及到的數(shù)據(jù)結(jié)構(gòu),再來具體剖析渲染的各個(gè)具體環(huán)節(jié)。
渲染過程中涉及到的關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)包括三棵樹和一個(gè)圖層,其中 RenderObject 持有了 Layer,我們重點(diǎn)先看一下三棵樹之間的關(guān)系。
舉個(gè)栗子,比如有這么一個(gè)簡單的布局:
那么對(duì)應(yīng)的三棵樹之間的關(guān)系如下圖所示:
第一棵樹,是 Widget Tree。它是控件實(shí)現(xiàn)的基本邏輯單位,是用戶對(duì)界面 UI 的描述方式。
需要注意的是,Widget 是不可變的(immutable),當(dāng)視圖配置信息發(fā)生變化時(shí),F(xiàn)lutter 會(huì)重建 Widget 來進(jìn)行更新,以數(shù)據(jù)驅(qū)動(dòng) UI 的方式構(gòu)建簡單高效。
那為什么將 Widget Tree 設(shè)計(jì)為 immutable?Flutter 界面開發(fā)是一種響應(yīng)式編程,主張“simple is fast”,而由上到下重新創(chuàng)建 Widget Tree 來進(jìn)行刷新,這種思路比較簡單,不用額外關(guān)系數(shù)據(jù)更變了會(huì)影響到哪些節(jié)點(diǎn)。另外,Widget 只是一個(gè)配置是數(shù)據(jù)結(jié)構(gòu),創(chuàng)建是輕量的,銷毀也是做過優(yōu)化的,不用擔(dān)心整棵樹重新構(gòu)建帶來的性能問題。
第二棵樹,Element Tree。它是 Widget 的實(shí)例化對(duì)象(如下圖,Widget 提供了 createElement 工廠方法來創(chuàng)建 Element),持久存在于運(yùn)行時(shí)的 Dart 上下文之中。它承載了構(gòu)建的上下文數(shù)據(jù),是連接結(jié)構(gòu)化的配置信息到最終完成渲染的橋梁。
之所以讓它持久地存在于 Dart 上下文中而不是像 Widget 重新構(gòu)建,**因?yàn)?Element Tree 的重新創(chuàng)建和重新渲染的開銷會(huì)非常大,**所以 Element Tree 到 RenderObject Tree 也有一個(gè) Diff 環(huán)節(jié),來計(jì)算最小重繪區(qū)域。
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
final Key key;
@protected
@factory
Element createElement();
/// ... 省略其他代碼
}
需要注意的是,Element 同時(shí)持有 Widget 和 RenderObject,但無論是 Widget 還是 Element,其實(shí)都不負(fù)責(zé)最后的渲染,它們只是“發(fā)號(hào)施令”,真正對(duì)配置信息進(jìn)行渲染的是 RenderObject。
第三棵樹,RenderObject Tree,即渲染對(duì)象樹。RenderObject 由 Element 創(chuàng)建并關(guān)聯(lián)到 Element.renderObject 上(如下圖),它接受 Element 的信息同步,同樣的,它也是持久地存在 Dart Runtime 的上下文中,是主要負(fù)責(zé)實(shí)現(xiàn)視圖渲染的對(duì)象。
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
RenderObject Tree 在 Flutter 的展示過程分為四個(gè)階段:
其中,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機(jī)制遍歷渲染對(duì)象樹,確定樹中各個(gè)對(duì)象的位置和尺寸,并把它們繪制到不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 處理。
那么問題來了,為什么是三棵樹而不是兩棵?為什么需要中間的 Element Tree,由 Widget Tree 直接構(gòu)建 RenderObject Tree 不可以嗎?
理論上可以,但實(shí)際不可行。因?yàn)槿绻苯訕?gòu)建 RenderObject Tree 會(huì)極大地增加渲染帶來的性能損耗。因?yàn)?Widget Tree 是不可變的,但 Element 卻是可變的。**實(shí)際上,Element 這一層將 Widget 樹的變化做了抽象(類似 React / Vue 的 VDOM Diff),只將真正需要修改的部分同步到 RenderObject Tree 中,由此最大程度去降低重繪區(qū)域,提高渲染效率。**可以發(fā)現(xiàn),F(xiàn)lutter 的思想很大程度上是借鑒了前端響應(yīng)式框架 React / Vue。
此外,再擴(kuò)展補(bǔ)充一下 VDOM。我們知道,Virtual DOM 的幾個(gè)優(yōu)勢是:
最后,看看 Layer,它依附于 RenderObject(通過 RenderObject.layer 獲?。抢L圖操作的載體,也可以緩存繪圖操作的結(jié)果。Flutter 分別在不用的圖層上繪圖,然后將這些緩存了繪圖結(jié)果的圖層按照規(guī)則進(jìn)行疊加,得到最終的渲染結(jié)果,也就是我們所說的圖像。
/// src/rendering/layer.dart
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// ... 省略無關(guān)代碼
bool get alwaysNeedsAddToScene => false;
bool _needsAddToScene = true;
void markNeedsAddToScene() {
_needsAddToScene = true;
}
bool _subtreeNeedsAddToScene;
void updateSubtreeNeedsAddToScene() {
_subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
}
}
如上圖代碼所示,Layer 的基類上有兩個(gè)屬性 _needsAddToScene 和 _subtreeNeedsAddToScene,前者表示需要加入場景,后者表示子樹需要加入場景。通常,只有狀態(tài)發(fā)生了更新,才需要加入到場景,所以這兩個(gè)屬性又可以直觀理解為「自己需要更新」和「子樹需要更新」。
Layer 提供了 markNeedsAddToScene() 來把自己標(biāo)記為「需要更新」。派生類在自己狀態(tài)發(fā)生變化時(shí)調(diào)用此方法把自己標(biāo)記為「需要更新」,比如 ContainerLayer 的子節(jié)點(diǎn)增刪、OpacityLayer 的透明度發(fā)生變化、PictureLayer 的 picture 發(fā)生變化等等。
繪制流程分為以下六個(gè)階段:
拋開 Diff 和 Render 我們本文不講解,因?yàn)檫@兩部分稍稍繁瑣一些,我們來關(guān)注下剩下的四個(gè)環(huán)節(jié)。
執(zhí)行 build 方法時(shí),根據(jù)組件的類型,存在兩種不同的邏輯。
我們知道,F(xiàn)lutter 內(nèi)的 Widget 可以分為 StatelessWidget 與 StatefulWidget,即無狀態(tài)組件與有狀態(tài)組件。
所謂 StatelessWidget,就是它 build 的信息完全由配置參數(shù)(入?yún)ⅲ┙M成,換句話說,它們一旦創(chuàng)建成功就不再關(guān)心、也不響應(yīng)任何數(shù)據(jù)變化進(jìn)行重繪。
所謂 StatefulWidget,除了父組件初始化時(shí)傳入的靜態(tài)配置之外,還要處理用戶的交互與內(nèi)部數(shù)據(jù)變化(如網(wǎng)絡(luò)數(shù)據(jù)回包)并體現(xiàn)在 UI 上,這類組件就需要以 State 類打來 Widget 構(gòu)建的設(shè)計(jì)方式來實(shí)現(xiàn)。它由 State 的 build 方法構(gòu)建 UI, 最終調(diào)用 buildScope 方法。其會(huì)遍歷 _dirtyElements,對(duì)其調(diào)用 rebuild/build。
只有布局類 Widget 會(huì)觸發(fā) layout(如 Container、Padding、Align 等)。
每個(gè) RenderObject 節(jié)點(diǎn)需要做兩件事:
/// 實(shí)際計(jì)算 layout 的實(shí)現(xiàn)
void performLayout() {
_size = configuration.size;
if (child != null) {
child.layout(BoxConstraints.tight(_size));
}
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
/// ...省略無關(guān)邏輯
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
performResize();
}
RenderObject debugPreviousActiveLayout;
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
如此遞歸一輪,每個(gè)節(jié)點(diǎn)都受到父節(jié)點(diǎn)的約束并計(jì)算出自己的 size,然后父節(jié)點(diǎn)就可以按照自己的邏輯決定各個(gè)子節(jié)點(diǎn)的位置,從而完成整個(gè) Layout 環(huán)節(jié)。
渲染管道中首先找出需要重繪的 RenderObject,如果有實(shí)現(xiàn)了 CustomPainter 則調(diào)用 CustomPainter paint 方法 再調(diào)用 child 的 paint 方法;如果未實(shí)現(xiàn) CustomPainter,則直接調(diào)用 child 的 paint。
在調(diào)用 paint 的時(shí)候,經(jīng)過一串的轉(zhuǎn)換后,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上。
void paint(PaintingContext context, Offset offset) {
if (_painter != null) {
// 只有持有 CustomPainter 情況下,才繼續(xù)往下調(diào)用自定義的 CustomPainter 的 paint 方法,把 canvas 傳過去
_paintWithPainter(context.canvas, offset, _painter);
_setRasterCacheHints(context);
}
super.paint(context, offset); //調(diào)用父類的paint的方法
if (_foregroundPainter != null) {
_paintWithPainter(context.canvas, offset, _foregroundPainter);
_setRasterCacheHints(context);
}
}
// 在父類的 paint 里面繼續(xù)調(diào)用 child 的 paint,實(shí)現(xiàn)父子遍歷
void paint(PaintingContext context, Offset offset) {
if (child != null){
context.paintChild(child, offset);
}
void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
int debugPreviousCanvasSaveCount;
canvas.save();
if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
// 在調(diào)用 paint 的時(shí)候,經(jīng)過一串的轉(zhuǎn)換后,layer->PaintingContext->Canvas,最終 paint 就是描繪在 Canvas 上
painter.paint(canvas, size);
/// ...
canvas.restore();
}
合成主要做三件事情:
final ui.Window _window;
void compositeFrame() {
// 省略計(jì)時(shí)邏輯
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
}
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
addChildrenToScene(builder);
}
void addChildrenToScene(ui.SceneBuilder builder, [ Offset childOffset = Offset.zero ]) {
Layer child = firstChild;
while (child != null) {
if (childOffset == Offset.zero) {
child._addToSceneWithRetainedRendering(builder);
} else {
child.addToScene(builder, childOffset);
}
child = child.nextSibling;
}
}
跨端開發(fā)是必然趨勢,從本質(zhì)上來說,它增加業(yè)務(wù)代碼的復(fù)用率,減少因?yàn)檫m配不同平臺(tái)帶來的工作量,從而降低開發(fā)成本。在各平臺(tái)差異抹平之前,要想“多快好省”地開發(fā)出各端體驗(yàn)接近一致的程序,那便是跨端開發(fā)了。
總得來說,業(yè)內(nèi)普遍認(rèn)同跨端方案存在以下三種:
下面來一一講解。
所謂 Web 容器,即是基于 Web 相關(guān)技術(shù)通過瀏覽器組件來實(shí)現(xiàn)界面和功能,包括我們通常意義上說的基于 WebView 的 “H5”、Cordova、Ionic、微信小程序。
這類 Hybrid 開發(fā)模式,只需要將開發(fā)一次 Web,就可以同時(shí)在多個(gè)系統(tǒng)的瀏覽器組件中運(yùn)行,保持基本一致的體驗(yàn),是迄今為止熱度很高的跨端開發(fā)模式。而 Web 與 原生系統(tǒng)之間的通信,則通過 JSBridge 來完成,原生系統(tǒng)通過 JSBridge 接口暴露能力給 Web 調(diào)用。而頁面的呈現(xiàn),則由瀏覽器組件按照標(biāo)準(zhǔn)的瀏覽器渲染流程自行將 Web 加載、解析、渲染。
這類方案的優(yōu)點(diǎn):簡單、天然支持熱更新、生態(tài)繁榮、兼容性強(qiáng)、開發(fā)體驗(yàn)友好。
當(dāng)然,缺點(diǎn)也很明顯,否則就沒有后面兩個(gè)方案什么事了,主要是體驗(yàn)上的問題:
所以輪到泛 Web 容器方案出場了,代表性框架是 React Native,Weex,Hippy。
在跨端通信上,React Native 依然通過 Bridge 的方式來調(diào)用原生提供的方法。
這套方案理想是美好的,但現(xiàn)實(shí)確實(shí)骨感的,它在實(shí)踐下來之后也依然發(fā)現(xiàn)了問題:
那我們究竟能不能既簡單地抹平差異,又同時(shí)保證性能呢?
答案是可以,那就是自繪引擎。不調(diào)用原生控件,我們自己去畫。那就是 Flutter。好比警察問 React Native 嫌疑犯長什么樣子,React Native 只能繪聲繪色地去描繪嫌疑犯的外觀,警察畫完之后再拿給 React Native 看,React Native 還要回答像不像;但 Flutter 自己就是一個(gè)素描大師,它可以自己將嫌疑犯的畫像畫好然后交給警察看。這兩者的效率和表現(xiàn)差異,不言而喻。
通過這樣的思路,F(xiàn)lutter 可以盡可能地減少不同平臺(tái)之間的差異, 同時(shí)保持和原生開發(fā)一樣的高性能。并且對(duì)于系統(tǒng)能力,可以通過開發(fā) Plugin 來支持 Flutter 項(xiàng)目間的復(fù)用。所以說,F(xiàn)lutter 成了三類跨端方案中最靈活的那個(gè),也成了目前業(yè)內(nèi)受到關(guān)注的框架。
至于通信效率,F(xiàn)luter 跨端的通信效率也是高出 JSBridge 許許多多。Flutter 通過 Channel 進(jìn)行通信,其中:
其中,MethodChannel 在開發(fā)中用的比較多,下圖是一個(gè)標(biāo)準(zhǔn)的 MethodChannel 的調(diào)用原理圖:
但為什么我們說 Channel 的性能高呢?梳理一下 MethodChannel 調(diào)用時(shí)的調(diào)用棧,如下圖所示:
可以發(fā)現(xiàn),整個(gè)流程中都是機(jī)器碼的傳遞,而 JNI 的通信又和 JavaVM 內(nèi)部通信效率一樣,整個(gè)流程通信的流程相當(dāng)于原生端的內(nèi)部通信。但是也存在瓶頸。我們可以發(fā)現(xiàn),methodCall 需要編解碼,其實(shí)主要的消耗都在編解碼上了,因此,MethodChannel 并不適合傳遞大規(guī)模的數(shù)據(jù)。
比如我們想調(diào)用攝像頭來拍照或錄視頻,但在拍照和錄視頻的過程中我們需要將預(yù)覽畫面顯示到我們的 Flutter UI中,如果我們要用 MethodChannel 來實(shí)現(xiàn)這個(gè)功能,就需要將攝像頭采集的每一幀圖片都要從原生傳遞到 Dart 側(cè)中,這樣做代價(jià)將會(huì)非常大,因?yàn)閷D像或視頻數(shù)據(jù)通過消息通道實(shí)時(shí)傳輸必然會(huì)引起內(nèi)存和 CPU 的巨大消耗。為此,F(xiàn)lutter 提供了一種基于 Texture 的圖片數(shù)據(jù)共享機(jī)制。
Texture 和 PlatformView 不在本文的探討范圍內(nèi),這里就不再深入展開了,有興趣的讀者可以自行查閱相關(guān)資料作為擴(kuò)展知識(shí)了解。
那接下來,我們就進(jìn)入本文的第三篇章吧,F(xiàn)lutter 混合開發(fā)模式的探索。
Flutter 混合工程的結(jié)構(gòu),主要存在以下兩種模式:
所謂統(tǒng)一管理模式,就是一個(gè)標(biāo)準(zhǔn)的 Flutter Application 工程,而其中 Flutter 的產(chǎn)物工程目錄(ios/ 和 android/ )是可以進(jìn)行原生混編的工程,如 React Native 進(jìn)行混合開發(fā)那般,在工程項(xiàng)目中進(jìn)行混合開發(fā)就好。但是這樣的缺點(diǎn)是當(dāng)原生項(xiàng)目業(yè)務(wù)龐大起來時(shí),F(xiàn)lutter 工程對(duì)于原生工程的耦合就會(huì)非常嚴(yán)重,當(dāng)工程進(jìn)行升級(jí)時(shí)會(huì)比較麻煩。因此這種混合模式只適用于 Flutter 業(yè)務(wù)主導(dǎo)、原生功能為輔的項(xiàng)目。但早期 Google 未支持 Flutter Module 時(shí),進(jìn)行混合開發(fā)也只存在這一種模式。
后來 Google 對(duì)混合開發(fā)有了更好的支持,除了 Flutter Application,還支持 Flutter Module。所謂 Flutter Module,恰如其名,就是支持以模塊化的方式將 Flutter 引入原生工程中,**它的產(chǎn)物就是 iOS 下的 Framework 或 Pods、Android 下的 AAR,原生工程就像引入其他第三方 SDK 那樣,使用 Maven 和 Cocoapods 引入 Flutter Module 即可。**從而實(shí)現(xiàn)真正意義上的三端分離的開發(fā)模式。
為了問題的簡潔性,我們這里暫時(shí)不考慮生命周期的統(tǒng)一性和通信層的實(shí)現(xiàn),而除此之外,混合導(dǎo)航棧主要需要解決以下四種場景下的問題:
Native -> Flutter,這種情況比較簡單,F(xiàn)lutter Engine 已經(jīng)為我們提供了現(xiàn)成的 Plugin,即 iOS 下的 FlutterViewController 與 Android 下的 FlutterView(自行包裝一下可以實(shí)現(xiàn) FlutterActivity),所以這種場景我們直接使用啟動(dòng)了的 Flutter Engine 來初始化 Flutter 容器,為其設(shè)置初始路由頁面之后,就可以以原生的方式跳轉(zhuǎn)至 Flutter 頁面了。
// Existing code omitted.
// 省略已經(jīng)存在的代碼
- (void)showFlutter {
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
Flutter -> Flutter,業(yè)內(nèi)存在兩種方案,后續(xù)我們會(huì)詳細(xì)介紹到,分別是:
Flutter -> Native,需要注意的時(shí),這里的跳轉(zhuǎn)其實(shí)是包含了兩種情況:
如上圖,這種情況相對(duì)復(fù)雜,我們需要使用 MethodChannel 讓 Dart 與 Platform 側(cè)進(jìn)行通信,Dart 發(fā)出 open 或 close 的指令后由原生側(cè)執(zhí)行相應(yīng)的邏輯。
Native -> Native,這種情況沒有什么好說的,直接使用原生的導(dǎo)航棧即可。
為了解決混合棧問題,以及彌補(bǔ) Flutter 自身對(duì)混合開發(fā)支持的不足,業(yè)內(nèi)提出了一些混合??蚣?,總得來說,離不開這四種混合模式:
下面,一一來談?wù)勊鼈兊脑砼c優(yōu)缺點(diǎn)。
Flutter Boost 是閑魚團(tuán)隊(duì)開源的 Flutter 混合框架,成熟穩(wěn)定,業(yè)內(nèi)影響力高,在導(dǎo)航棧的處理思路上沒有繞開我們?cè)?3.2 節(jié)中談及的混合棧原理,但需要注意的是,當(dāng) Flutter 跳轉(zhuǎn) Flutter 時(shí),它采用的是 new 一個(gè)新的 FlutterViewController 后使用原生導(dǎo)航棧跳轉(zhuǎn)的方式,如下圖所示:
這么做的好處是使用者(業(yè)務(wù)開發(fā)者)操作 Flutter 容器就如同操作 WebView 一樣,而 Flutter 頁面就如同 Web 頁面,邏輯上簡單清晰,將所有的導(dǎo)航路由邏輯收歸到原生側(cè)處理。如下圖,是調(diào)用 open 方法時(shí) Flutter Boost 的時(shí)序圖(關(guān)鍵函數(shù)路徑),這里可以看到兩點(diǎn)信息:
但是它也有缺點(diǎn),就是每次打開 Flutter 頁面都需要 new 一個(gè) ViewController,在連續(xù)的 Flutter 跳轉(zhuǎn) Flutter 的場景下有額外的內(nèi)存開銷。針對(duì)這個(gè)問題,又有團(tuán)隊(duì)開發(fā)了 Flutter Thrio。
上面我們說到,F(xiàn)lutter 跳轉(zhuǎn) Flutter 這種場景 Flutter Boost 存在額外的內(nèi)存開銷,故哈啰出行團(tuán)隊(duì)今年4月開源了 Flutter Thrio 混合框架,其針對(duì) Flutter Boost 做出的最重要的改變?cè)谟冢篎lutter 跳轉(zhuǎn) Flutter 這種場景下,Thrio 使用了 Flutter Navigator 導(dǎo)航棧。如下圖所示:
在連續(xù)的 Flutter 頁面跳轉(zhuǎn)場景下,內(nèi)存測試圖表如下:
從這張圖表中我們可以得到以下幾點(diǎn)信息:
可見,在這種場景下,Thrio 還是做出了一定的優(yōu)化的。但與之帶來的,就是實(shí)現(xiàn)的復(fù)雜性。我們談到 Flutter Boost 的優(yōu)點(diǎn)是簡單,路由全部收歸原生導(dǎo)航棧。而 Flutter Thrio 混用了原生導(dǎo)航棧和 Flutter Navigator,因此實(shí)現(xiàn)會(huì)相對(duì)更復(fù)雜一下。這里我梳理了一下 Flutter Thrio open 時(shí)關(guān)鍵函數(shù)路徑,可以看到,Thrio 的導(dǎo)航管理確實(shí)是復(fù)雜了一些。
以上我們談及的兩種混合框架都是單引擎的,對(duì)應(yīng)的,也存在多引擎的框架。在談多引擎之前,還是需要先介紹一下關(guān)于 Engine、Dart VM、isolate 幾個(gè)前置知識(shí)點(diǎn)。
在第一篇章中我們沒有涉及到 Engine 層的源碼分析,而著重篇幅去講解 Framework 層的原理,一是為了第一章的連貫性,二是此處也會(huì)單獨(dú)說到 Engine,還是最好放在此時(shí)講解會(huì)更便于記憶與理解。
Dart VM、Engine 與 isolate
(a)Dart 虛擬機(jī)創(chuàng)建完成之后,需要?jiǎng)?chuàng)建 Engine 對(duì)象,然后會(huì)調(diào)用
DartIsolate::CreateRootIsolate() 來創(chuàng)建 isolate。 (b)每一個(gè) Engine 實(shí)例都為 UI、GPU、IO、Platform Runner 創(chuàng)建各自新的 Thread。 (c)isolate,顧名思義,內(nèi)存在邏輯上是隔離的。 (d)isolate 中的 code 是按順序執(zhí)行的,任何 Dart 程序的并發(fā)都是運(yùn)行多個(gè) isolate 的結(jié)果。當(dāng)然我們可以開啟多個(gè) isolate 來處理 CPU 密集型任務(wù)。
根據(jù)(a)我們可以推出:(1) 每個(gè) Engine 對(duì)應(yīng)一個(gè) isolate 對(duì)象,即 Root Isolate。 根據(jù)(b)我們可以推出:(2) Engine 是一個(gè)比較重的對(duì)象(前文也有所提及)。 根據(jù)(c)和 (1) 我們可以推出:(3) Engine 與 Engine 之間相互隔離。 根據(jù)(d)和 (3) 我們可以推出:(4) Engine 沒有共享內(nèi)存的并發(fā),沒有競爭的可能性,不需要鎖,也就不存在死鎖問題。
好啦,記住這四個(gè)結(jié)論,我們?cè)賮砜纯?window。
window 是繪圖的窗口,也是連接 Flutter Framework(Dart)與 Flutter Engine(C++)的窗口 (5)。
從類的定義上來看,window 是連接 Framework 與 Engine 的窗口。在 Framework 層,window 指的是 ui.window 單例對(duì)象,源碼文件是 window.dart。而在 Engine 層,源碼文件是 window.cc,兩者交互的 API 很少,但是一一對(duì)應(yīng):
可以發(fā)現(xiàn),這些主要是 Framework 層調(diào)用 Engine 層中 Skia 庫封裝后的相關(guān) API。那就不得不說說它的第二層含義——作為繪圖的窗口。
從功能上來看,在界面繪制交互意義上,window 也是繪圖的窗口。在 Engine 中,繪圖操作輸出到了一個(gè) PictureRecorder 的對(duì)象上;在此對(duì)象上調(diào)用 endRecording() 得到一個(gè) Picture 對(duì)象,然后需要在合適的時(shí)候把 Picture 對(duì)象添加(add)到 SceneBuilder 對(duì)象上;調(diào)用 SceneBuilder 對(duì)象的 build() 方法獲得一個(gè) Scene 對(duì)象;最后,在合適的時(shí)機(jī)把 Scene 對(duì)象傳遞給 window.render() 方法,最終把場景渲染出來。
實(shí)例代碼如下:
import 'dart:ui';
void main(){
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
Paint p = Paint();
p.strokeWidth = 30.0;
p.color = Color(0xFFFF00FF);
canvas.drawLine(Offset(300, 300), Offset(800, 800), p);
Picture picture = recorder.endRecording();
SceneBuilder sceneBuilder = SceneBuilder();
sceneBuilder.pushOffset(0, 0);
sceneBuilder.addPicture(new Offset(0, 0), picture);
sceneBuilder.pop();
Scene scene = sceneBuilder.build();
window.onDrawFrame = (){
window.render(scene);
};
window.scheduleFrame();
}
多 Engine 模式
綜上,根據(jù)(1)(3)(5)我們可以得出下圖的多引擎模式:
它有以下幾個(gè)特征:
根據(jù)這三個(gè)特征,我們可以設(shè)想一下其通信層的實(shí)現(xiàn),假設(shè)存在兩個(gè)引擎,每個(gè)引擎內(nèi)又存在兩個(gè) FlutterVC,每個(gè) FlutterVC 內(nèi)又存在兩個(gè) Flutter 頁面,那這種場景下的跳轉(zhuǎn)就會(huì)變得非常復(fù)雜(下圖出自 Thrio 開源倉庫中的README):
所以顯而易見的,我們不可否認(rèn) Engine 之間的邏輯隔離帶來了模塊間天然的隔離性,但是問題也有許多:
首先如上圖所示,通信層設(shè)計(jì)會(huì)異常復(fù)雜,而且通信層的核心邏輯依然是需要放在原生側(cè)來實(shí)現(xiàn),如此便一定程度上失去了跨端開發(fā)的優(yōu)勢。
其次,我們反復(fù)提到 Engine 是一個(gè)比較重的對(duì)象,啟動(dòng)多個(gè) Flutter Engine 會(huì)導(dǎo)致資源消耗過多。
最后,由于 Engine 之間沒有共享內(nèi)存,這種天然的隔離性其實(shí)弊大于利,在混合開發(fā)的視角下,一個(gè) App 需要維護(hù)兩套緩存池——原生緩存池與 DartVM 所持有的緩存池,但是隨著開啟多 Engine 的介入,后者緩存池的資源又互不相通,導(dǎo)致資源開銷變得更加巨大。
為了解決傳統(tǒng)的多 Engine 模式所帶來的這些問題,又有團(tuán)隊(duì)提出了基于 View 級(jí)別的混合模式。
基于 View 級(jí)別的混合模式,核心是為每個(gè) window 加入 windowId 的概念,以便它們?nèi)ス蚕硗环?Root Isolate。我們剛才說到,一個(gè) isolate 具有一個(gè) ui.window 單例對(duì)象,那么只需要做一點(diǎn)修改,把 Flutter Engine 加入 ID 的概念傳給 Dart 層,讓 Dart 層存在多個(gè) window,就可以實(shí)現(xiàn)多個(gè) Flutter Engine 共享一個(gè) isolate 了。
如下圖所示:
這樣就可以真正實(shí)現(xiàn) View 級(jí)別的混合開發(fā),可以同時(shí)持有多份 FlutterViewController,且這些 FlutterVC 可以內(nèi)存共享。
那缺點(diǎn)也比較明顯,我們需要對(duì) Engine 代碼做出修改,維護(hù)成本會(huì)很高。其次,多 Engine 的資源消耗問題在這種模式下也是需要通過對(duì) Engine 不斷裁剪來解決的。
Dart 天然支持兩種編譯模式,JIT 與 AOT。
所謂 JIT,Just In Time,即時(shí)編譯/運(yùn)行時(shí)編譯,在 Debug 模式中使用,可以動(dòng)態(tài)下發(fā)和執(zhí)行代碼,但是執(zhí)行性能受運(yùn)行時(shí)編譯影響。
所謂 AOT,Ahead Of Time,提前編譯/運(yùn)行前編譯,在 Release 模式中使用,可以為特定平臺(tái)生成二進(jìn)制代碼,執(zhí)行性能好、運(yùn)行速度快,但每次執(zhí)行都需要提前編譯,開發(fā)調(diào)試效率低。
對(duì)應(yīng)的 Flutter App 存在三種運(yùn)行模式:
因此,我們可以看出,在開發(fā)調(diào)試過程中,我們需要使用支持 JIT 的 Debug 模式,而在生產(chǎn)環(huán)境中,我們需要構(gòu)建包為支持 AOT 的 Release 模式以保證性能。
那么,這對(duì)我們的集成與構(gòu)建也提出了一定的要求。
所謂集成,指的是混合項(xiàng)目中,將 Flutter Module 的產(chǎn)物集成到原生項(xiàng)目中去,存在兩種集成方式,區(qū)別如下:
可以發(fā)現(xiàn)源碼集成是 Flutter dev 分支需要的,但是產(chǎn)物集成是 Flutter dev 以外的分支需要的。在這里,我們的混合項(xiàng)目需要同時(shí)支持兩種不同的集成工程,在 Flutter dev 分支上進(jìn)行源碼集成開發(fā),然后依賴抽取構(gòu)建產(chǎn)物發(fā)布到遠(yuǎn)程,如 iOS 構(gòu)建成 pods 發(fā)布到 Cocoapods 對(duì)應(yīng)的倉庫,而 Android 構(gòu)建成 AAR 發(fā)布到 Maven 對(duì)應(yīng)的云端。于是,其他分支的工程直接 gradle 或者 pod install 就可以更新 Flutter 依賴模塊了。
當(dāng)然,我們說到運(yùn)行模式存在 Debug、Release、Profile 三種,其對(duì)應(yīng)的集成產(chǎn)物也會(huì)區(qū)分這三種版本,但由于產(chǎn)物集成無法調(diào)試,集成 Debug 版本和 Profile 版本沒有意義,因此依賴抽取發(fā)布時(shí)只需要發(fā)布 Release 版本的產(chǎn)物就好。
在整套「Fan 直播」Flutter 混合項(xiàng)目搭建之后,我們形成了一套初具雛形的 Flutter 工作流。在未來,我們也會(huì)不斷完善 Flutter 混合開發(fā)模式,積極參與到 Flutter 的生態(tài)建設(shè)中去。
熱門資訊
探討游戲引擎的文章,介紹了10款游戲引擎及其代表作品,涵蓋了RAGE Engine、Naughty Dog Game Engine、The Dead Engine、Cry Engine、Avalanche Engine、Anvil Engine、IW Engine、Frostbite Engine、Creation引擎、Unreal Engine等引擎。借此分析引出了游戲設(shè)計(jì)領(lǐng)域和數(shù)字藝術(shù)教育的重要性,歡迎點(diǎn)擊咨詢報(bào)名。
2. 手機(jī)游戲如何開發(fā)(如何制作傳奇手游,都需要準(zhǔn)備些什么?)
?如何制作傳奇手游,都需要準(zhǔn)備些什么?提到傳奇手游相信大家都不陌生,他是許多80、90后的回憶;從起初的端游到現(xiàn)在的手游,說明時(shí)代在進(jìn)步游戲在更新,更趨于方便化移動(dòng)化。而如果我們想要制作一款傳奇手游的
3. B站視頻剪輯軟件「必剪」:免費(fèi)、炫酷特效,小白必備工具
B站視頻剪輯軟件「必剪」,完全免費(fèi)、一鍵制作炫酷特效,適合新手小白??靵碓囋?!
4. Steam值得入手的武俠游戲盤點(diǎn),各具特色的快意江湖
游戲中玩家將面臨武俠人生的掙扎抉擇,戰(zhàn)或降?殺或放?每個(gè)抉定都將觸發(fā)更多愛恨糾葛的精彩奇遇?!短烀嬗肪哂卸嗑€劇情多結(jié)局,不限主線發(fā)展,高自由...
5. Bigtime加密游戲經(jīng)濟(jì)體系揭秘,不同玩家角色的經(jīng)濟(jì)活動(dòng)
Bigtime加密游戲經(jīng)濟(jì)模型分析,探討游戲經(jīng)濟(jì)特點(diǎn),幫助玩家更全面了解這款GameFi產(chǎn)品。
6. 3D動(dòng)畫軟件你知道幾個(gè)?3ds Max、Blender、Maya、Houdini大比拼
當(dāng)提到3D動(dòng)畫軟件或動(dòng)畫工具時(shí),指的是數(shù)字內(nèi)容創(chuàng)建工具。它是用于造型、建模以及繪制3D美術(shù)動(dòng)畫的軟件程序。但是,在3D動(dòng)畫軟件中還包含了其他類型的...
7. 3D動(dòng)漫建模全過程,不是一般人能學(xué)的會(huì)的,會(huì)的多不是人?
步驟01:面部,頸部,身體在一起這次我不準(zhǔn)備設(shè)計(jì)圖片,我從雕刻進(jìn)入。這一次,它將是一種純粹關(guān)注建模而非整體繪畫的形式。像往常一樣,我從Sphere創(chuàng)建它...
8. 如何自己開發(fā)一款游戲(游戲開發(fā)入門必看:五大獨(dú)立游戲開發(fā)技巧)
?游戲開發(fā)入門必看:五大獨(dú)立游戲開發(fā)技巧無論您是剛剛起步開發(fā)自己的第一款游戲,還是已經(jīng)制作了幾款游戲,本篇文章中的5大獨(dú)立游戲開發(fā)技巧都可以幫助您更好地設(shè)計(jì)下一款游戲。無論你對(duì)游戲有著什么樣的概念,都
?三昧動(dòng)漫對(duì)于著名ARPG游戲《巫師》系列,最近CD Projekt 的高層回應(yīng)并不會(huì)推出《巫師4》。因?yàn)椤段讕煛废盗性诓邉澋臅r(shí)候一直定位在“三部曲”的故事框架,所以在游戲的出品上不可能出現(xiàn)《巫師4》
10. 3D打印技巧揭秘!Cura設(shè)置讓你的模型更堅(jiān)固
想讓你的3D打印模型更堅(jiān)固?不妨嘗試一下Cura參數(shù)設(shè)置和設(shè)計(jì)技巧,讓你輕松掌握!
最新文章
同學(xué)您好!