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