Flutter 跨端技術一經推出便在業內贏得了不錯的口碑,它在「多端一致」和「渲染性能」上的優點讓其餘跨端方案很難比擬。雖然 Flutter 的成長曲線和將來前景看起來都很好,但不能否認的是,目前 Flutter 仍處在發展階段,不少大型互聯網企業都沒法毫無顧慮地讓全線 App 接入,而其中最主要的顧慮是包大小與動態化。前端
動態化表明着更短的需求上線路徑,表明着大大壓縮了原始包的大小,從而得到更高的用戶下載意向,也表明着更健全的線上質量維護體系。當明白這些意義後,咱們也就不難理解,在 Flutter 的應用與適配趨近完善時,動態化天然就成爲了一個沒法避開的話題。RN 和 Weex 等成熟技術甚至讓你們認爲動態化是跨端技術的標配。git
美團外賣 MTFlutter 團隊從 2019 年 9 月開始對動態化進行研究,目前已在多個業務模塊上線,內部項目代號 「Flap」 。。github
Flap 研發的初心是爲了提供一個完整解決方案,而不是一個過渡方案。項目組思考了當下最痛的點並逐一列出,而後再根據目標來作具體選型。在前期,只有需求考慮得越周全,後續的架構和研發纔會越明確。在研發過程當中,團隊應該堅守底線,堅守初心,不斷攻克困難,完成昔日定下的目標。編程
a. 產物替換緩存
選型中首先考慮到的是下發產物替換,官方在也曾經推出了 Code Push 方案,甚至能夠支持 Diff 差量下載,可是在 2019 年 4 月被叫停,這裏引用一下官方的發言 Flutter/issues/14330:安全
To comply with our understanding of store policies on Android and iOS, any solution would be limited to JIT code on Android and interpreted code on iOS. We are not confident that the performance characteristics of such a solution on iOS would reach the quality that we demand of our product. (In other words, "it would be too slow".)微信
There are some serious security concerns. Since these patches would essentially allow arbitrary code execution, they would be extremely attractive malware vectors. We could mitigate this by requiring that patches be signed using the same key as the original package, but this is error prone and any mistake would have serious consequences. This is, fundamentally, the same problem that has plagued platforms that allow execution of code from third-party sources. This problem could be mitigated by integrating with a platform update mechanism, but this defeats the purpose of an out-of-band patching mechanism.數據結構
簡而言之,就是官方對動態化後的性能沒有自信,而且對安全性有所顧慮。以前,官方提供方案的侷限性也十分明顯。好比對 Native-Flutter 混合 App 支持不友好,而且沒法進行灰度等業務定製操做,因此不能知足通用性和高性能的核心目標。架構
b. AOT 搭載 JITapp
Flutter 在 Release 模式下構建的是 AOT 編譯產物,iOS 是 AOT Assembly,Android 默認 AOTBlob。 同時 Flutter 也支持 JIT Release 模式,能夠動態加載 Kernel snapshot 或 App-JIT snapshot。若是在 AOT 上支持 JIT,就能夠實現動態化能力。但問題在於,AOT 依賴的 Dart VM 和 JIT 並不同,AOT 須要一個編譯後的 「Dart VM」(更準確地說是 Precompiled Runtime),JIT 依賴的是 Dart VM(一個虛擬機,提供語言執行環境);而且 JIT Release 並不支持 iOS 設備,構建的應用也不能在 AppStore 上發佈。
實現此方案須要抽離一份 DartVM 獨立編譯,再以動態庫的形式引入項目。經過初步測試,發現會增大包體積 20MB+,這超過了 MTFlutter 以前作 Flutter 包體積優化的總和。進一步讓 Flutter 包體積成爲推廣與接入業務方的巨大阻礙,不知足咱們對適用性的要求。
c. 動態生產 DSL
Native 側自己具有 JS 動態執行環境,利用這個執行環境動態生成包含頁面和邏輯事件綁定 DSL,進而解析爲 Flutter 頁面或組件,也能夠實現動態化訴求。技術思路接近 RN,但與其不一樣的是利用 Flutter 渲染引擎和框架。這種先將代碼執行起來再獲取 DSL 的手段,咱們簡稱爲動態生產 DSL。
此方案能夠很好地支持邏輯動態化,但弊端也比較明顯。首先要對齊 Flutter 框架,JS 側的開發量很大且開發體驗受損。另外,對 JS 的依賴偏重,構建的 JS 框架自己解釋執行有必定開銷,對於頁面邏輯與事件在運行中須要頻繁地進行 Flutter 與 JS 的跨平臺通訊,一樣也會產生必定開銷。這不能知足 MTFlutter 團隊對高性能的訴求。更嚴重的是,此方案對開發同窗的開發習慣並不友好,將 Dart 改成 JS,現有的 Flutter 開發工具沒法直接使用,這與低成本訴求背道而馳。
d. 靜態生產 DSL
前面說 「將代碼執行起來再獲取 DSL 的手段,咱們簡稱爲動態生產 DSL」,那麼代碼不執行直接轉換 DSL,就稱爲靜態生產 DSL 方案。
靜態生產的特色是抹平了平臺差別,由於 input 是 Dart source 與平臺無關,直接將 Dart source 內的完整信息經過一層轉換器轉換到 DSL,而後經過 Native 和 Dart 的靜態映射和基礎的邏輯支持環境,使得其能夠在純 Dart 的環境下渲染與交互。
在具體實現上,能夠利用 Dart-lang 官方提供的 Analyzer 分析庫(該工具在 Dartfmt、Dart Doc、Dart Analyzer Server 中都有使用)構建 DSL。該庫提供了一組 API 能對 Dart source 進行分析,按照文件粒度生成 AST 對象。AST 對象用整齊的數據結構包含了 Dart 文件的全部信息,利用這些信息能夠便捷地生成所需的 DSL。全部的這個分析 + 轉換的過程所有在線下進行。接下來, DSL-JSON 以 Zip 的形式下發,Flutter 的 AOT 側以此爲數據源,完成整個 Flutter 項目的渲染與交互。
這種方案,一來能夠保持 Flutter/Dart 的開發體驗,也沒有平臺差別,邏輯動態化依賴靜態映射和基礎邏輯支持,而非 JScore,有效地避免了性能上的開銷。綜上考慮,靜態生產 DSL 最終成爲 MTFlutter 團隊選型的方案。
如圖 1 所示,三處淺綠色部分爲一個階段的階段產物,起到承上啓下的做用。以綠色部分爲界,總體架構天然而然的就被劃分紅了三個區域:
圖 1 中的核心模塊是轉換器部分和運行時部分,接下來會介紹下這兩個部分的原理與部分實現。
AST & DSL
AST 意爲抽象語法樹(Abstract Syntax Tree)。Dart 的 AST 和其餘語言的 AST 基本概念相似。'package:front_end/src/scanner/token.dart' 中定義了全部的 Token,AST 也是經過詞法分析、語法分析、解層級嵌套獲得。ASTNode 對象做爲存儲編譯單元中重要信息的基本數據結構,派生類基本分爲 Declaration、Expression、Literal、Statement。
DSL 意爲領域特定語言(Domain-specific Language)。表示專門針對特定問題領域的編程語言或者規範語言。相對天然語言,編程語言是不靈活的,它的語法和語義設計常取決於它的執行環境和特定目的。過去人們老是發明新的編程語言,近年來新出現的語言愈來愈相近,所以 DSL 也變得流行起來。
那 Flap 的 DSL 具體是什麼?對於開發者而言,那這個 DSL 就是 Dart Code。而對於機器或 App 而言,那這個 DSL 就是 JSON。
前面的技術選型中提到:
利用 Dart-lang 官方提供了 Analyzer 分析庫,官方的 Analyzer 的能力能夠拿來直接用,該庫提供了一組 API 能對 Dart source 進行分析,按照文件粒度生成 AST 對象,該數據結構包含了 input 的 Dart 文件的全部信息。
咱們的 DSL 的基本原理就是對 AST 內數據的一個描述, 並附帶一些其餘操做。
由於用 Analyzer 的 API 跑出的 AST 也叫 CompilationUnit,其實是一個編譯單元,裏面還存有不少編譯相關的屬性例如 lineInfo、beginToken 等。但使用 DSL 的方式不依賴編譯,因此不少不須要的屬性會被裁剪或忽略。
在轉換器入口會對大類(identifier、statementImpl、literal、methodInvocation 等等)進行分發,每個大類的數據結構使用一種中間結構 Dart model 來傳輸,而後對於大類中細分的類型(IfStatement、AssignmentStatement、DoStatement、SwitchStatement 等等),配有足夠細粒度的轉換接口,以 AST 結構做爲輸入,以 Map 節點做爲輸出。最終定義並提煉了 10 種標準的 Map 結構(class、method、variable、stmt 等等)來承載全部類型。
舉個例子
一個簡單的 Widget 節點通過轉換後獲得這樣的 DSL-JSON,能夠看到 DSL 的可讀性仍是 OK 的(默認下發時產物是一個壓縮成單行並加密的二進制文件,這裏是解密後 Format 換行後展現的)。咱們在轉換中會區分普通的字符串、變量名引用、系統枚舉等類型,加以不一樣的符號表示。
關於邏輯
舉一個簡單的四則運算的例子,能夠看出在對於「乘法應當先計算」這個規則上,咱們的 DSL 可以自動遵循, 其中的奧祕是 Analyzer 幫咱們作了這種運算優先級的判斷,歸根結底仍是一種描述 AST 的工做,咱們本身不會去根據靜態代碼作分析過程。
關於語法糖
語法糖每每畫風清奇,結構不同凡響,可是在 AST 中仍是很誠實的,該什麼結構就是什麼結構。因此語法糖應該在轉換器側進行展開爲常規結構再轉 DSL,而不是對特殊格式設置特殊的 DSL 傳到運行時再去解析。
這裏只舉了一些簡單的例子,只是 DSL 體系中的一個片斷,實際在項目落地時有不少較爲複雜的邏輯,相似於循環套循環內進行集合操做或是異步回調內加多重三目邏輯等等。這裏由於篇幅緣由和涉及到業務代碼相關就不展開詳細的介紹了,其中的原理是同樣的,都是描述 AST 的過程當中增長一些特殊處理,最終會將轉換產物的 Map 節點根據原有 AST 的層級結構組裝起來,再經過 JSONEncode 轉爲 JSON。
轉換器側可以完整的描述一個 Dart 文件的全部信息,如圖 6 所示。值得一提的是,不一樣的節點還可能出現任意結構,method 裏的 Argument 裏多是一個全局變量,條件表達式的右邊又多是一個方法。對於這種相同的結構即便出如今不一樣的位置也應當使用一套處理邏輯來轉換,所以轉換器是以迭代爲主加小範圍遞歸的設計思路。
將細粒度轉換接口按照具體類別分在不一樣文件中(statement_factory、class_factory、function_factory) 等待解析生產總線的調用。實際操做中各個類之間是近似於網狀的調用,所以全部調用應當都是 Static 的,而且內部隔離,不引用不修改外部變量,作到無反作用。
DSL 轉換器是一個命令行程序,所以能夠無縫的部署到自動化的機器上。新代碼合入主幹後, 接下來的 Bundle 生成與分發邏輯均可以使用各類圖形化界面的發佈系統來操做。
Prepare & Running
運行時相關的操做是在 App 內發生的,包括初始化,拉取 DSL,解析與使用。 簡言之能夠分爲 Prepare 和 Running 兩個階段。Prepare 是準備各類運行時所需的符號,包括系統類符號與自定義符號,屬性符號與方法符號(這裏所說的符號實際就是 Dart 內的對象)。Prepare 階段完成才能進行後續的 Running 相關操做,具體是頁面的構建,事件的綁定,交互與邏輯的正常運轉。
萬能方法 Function.apply()
Flutter 指望線上產品是編譯後的「徹底體現」,同時爲了不生成過大的包,並不支持 Dart:Mirror。「Flutter apps are pre-compiled for production, and binary size is always a concern with mobile apps, we disabled dart:mirrors.」那麼,在這種前提下,如何將外部符號轉內部符號?Function() 對象提供了這樣一個萬能方法。
// function.dart
external static apply(Function function, List positionalArguments,
[Map<Symbol, dynamic> namedArguments]);
複製代碼
第一個參數是 Function 類型,後兩個參數是該函數所需的參數(位置參數與命名參數,這二者在 DSL 中均可以取到),所以只要能獲取到某個 Function,那就能在任什麼時候候調用它。
此 Function 若爲 Constructor Function 那返回值則爲構造出的對象類型。
Proxy-Mirror
DSL 後只能獲得字符串的標識,所以須要創建一個 String 與 Function 的映射關係,考慮到類名方法名,數據結構應該是 {String:{String:Function}},經過 className 和 functionName 兩個 String Key 便可取得一一對應的 Function(),下面給出一個系統類的類方法(構造方法)的代碼片斷:
{
'EdgeInsets':
{
'fromLTRB': (left, top, right, bottom) => EdgeInsets.fromLTRB(left, top, right, bottom),
// ...other function
},
// ...other class
};
複製代碼
而後對於系統類的實例方法、getter、setter 則須要在外部多傳一個 instance 參數,instance 是外部經過該類的構造方法的 func 建立後傳入。
// instance method
"inflateSize": (instance, size) => instance.inflateSize(size),
// getter
"horizontal": (instance) => instance.horizontal,
// setter
"last": (List instance, dynamic value) => instance.last = value,
複製代碼
Custom Class's meta
對於自定義類,咱們須要構建一個模擬的元類系統,存放全部符號信息,在解析時將全部的 JSON 節點轉成可處理的對象。全部的屬性聲明都會構建成 FlapVariable 類型,全部的方法聲明都會構建成 FlapFunction 類型。
如圖 8 所示,父類和元類也是有相應的指針,父類的成員變量也會填充到子類,而且經過 mixin 的方式將類相關屬性注入到派生類類型,例如 FlapState,FlapState 繼承自 state,這樣既可讓系統類的生命週期方法留個調用鏈的開口,也可使用注入的運行時類屬性。
Evaluate
以下面代碼的例子,一個 if 語句的 JSON 節點下發後,通過 parser 以後會獲得一個 IfStatement 對象,這類對象都有一個特色就是包含幾個屬性,和一個運行時入口方法 evaluate(Scope scope)。這個方法在抽象類 Evaluative 類中,全部語句和表達式的類都會繼承於此,自動得到 evaluate 方法,其中屬性部分是在解析過程當中解析成 Dart 對象後經過構造方法的參數傳入的。
class IfStatement extends Statement {
dynamic condition = undefined;
Body thenBody;
Body elseBody;
IfStatement(this.condition, this.thenBody, [this.elseBody]);
// 簡化版代碼
ProcessResult evaluate(Scope scope) {
bool conditionValue = condition.evaluate(scope)
if (conditionValue){
return thenBody(Scope);
}else{
return elseBody(Scope);
}
}
}
複製代碼
屬性中的條件對象與語句對象在解析的過程當中並不會被觸發, 真正的觸發是方法被調用時從運行時的入口方法 evaluate 進入,此時纔會經過做用域 Scope 斷定條件是 true or false,而後調用到其餘須要 evaluate 的 Dart 對象,以下圖 9 所示:
通過表達式的堆疊,實現了語句,通過語句的堆疊實現了 body,再補充上形參和返回值,則就構成了咱們運行時中的自定義方法 FlapFunction。這裏要用到一下仿真函數的概念,FlapFunction 要實現 call 方法,這樣在外部調用時就真的和 Function 畫風一致了。
動態化頁面運行時,Flap 會維持一套做用域體系。Scope 的結構至關於雙向鏈表,每個 Scope 有 outer 和 inner 兩個指針。全局做用域的 outer 爲 null,inner 爲類做用域;類做用域的 inner 爲局部做用域;局部做用域的 inner 可能爲 null 也可能又是一個局部做用域;隨便哪個做用域順着 outer 一直往上找,確定能找到全局做用域。
Scope
Scope 在邏輯的執行中實際就是充當了 Context 上下文的做用,由於每一個方法或表達式被 evalute 時須要一個 Scope 入參,這個 Scope 是從外部傳入的,而且這一行語句對象執行後 Scope 還會做爲入參傳給下一行語句。好比第一行語句聲明瞭一個 「code」 的變量,第二行語句對這個 「code」 進行修改,則須要先經過引用從 Scope 中取出這個 「code」 的值,不但能夠從 Scope 中取出聲明的屬性,也能夠取出聲明過的方法,方法內也是能夠調用方法的。這也就解釋了爲何咱們能夠處理自定義方法中的邏輯。
圖 10 描述了 Scope 在實際運用中的兩種場景。左半部分是點擊按鈕觸發 onTap 回調,須要找到 confirm 方法,此時會先從局部做用域的方法列表裏找,沒找到,則會 outer 一層去類做用域裏尋找,此時找到了該方法的實現。
右半部分展現了執行該方法的 body 時是須要傳入的 Scope 是如何構建的。先從符號大本營中獲取全局變量、全局屬性構成全局做用域,再今後類的元類中取出屬性和方法構成類做用域,再構建局部做用域,固然參數也是會放到局部做用域裏的,以此構建了完整的 Scope 傳入 body 的 evaluate 方法支撐後面的邏輯執行。
工做量大,須要長期有耐心
首先解釋下,這裏的工做量大並非指系統方法映射等這種體力活的工做量大,這些咱們都是有自動生成且按需生成的(生態部分會提到)。咱們所說的工做量大,主要是指涵蓋轉換器、運行時的研發以及生態相關建設等,咱們要儘量的知足全部的 Dart 語法才能讓業務代碼可以低成本的轉換,而且有衆多的腳本與工具支撐。
項目複雜,須要設計合理的架構以支撐擴展
在項目的分模塊開發中,各個模塊(parser、intermediate、runtime 等等)嚴格遵照單一職責原則與最小知道原則,最大化的杜絕了模塊間耦合,模塊與模塊的通訊由一些標準的數據結構進行(map 或繼承自 ASTNode 的結構)。 這就使得任何一個模塊出現重大重構時不會影響到其餘模塊,其中底層核心的幾個類的單側覆蓋率接近100%,有專人負責優化。而且在項目中隨處能夠抽象類、接口類、mixin 類等,這也就使得隨着支持的能力愈來愈複雜時,項目的可讀性不會成反比,代碼不會變「噁心」,而是以整齊的方式擴張,文件多而不亂。
疑難雜症較多,對問題保持足夠的信心
有時候會遇到一些諸如靜態方法調用構造方法時做用域被覆蓋、循環語句嵌套時內側 continue 以後外側語句也會跟着停、某方法參數的 Function 取完引用以後 Function 也跟着執行了等等的 Bug,解 Bug 是開發中必不可少的一部分,有時候加個 if else 用 easy way 能夠很快解決,但咱們不會那麼作,探索優雅 Right Way 的樂趣是研發過程當中的一個重要組成部分。
相比於草草了事以後,每晚睡前都會面臨這段代碼「靈魂」拷問,咱們更願意多花時間思考把代碼寫的像 Mac pro 主機的包裝那樣「絲滑」。這樣的工做氛圍培養了每位同窗的信心,只要是必現問題,基本都能優雅地解決。
雖然 Flap 的設計理念使得其在開發效率與執行效率上有必定的亮點,但這還不足以讓其在業務中快速推廣。所以咱們建設了一套完整的 Flap 生態體系,涵蓋了開發、發佈、測試、運維各階段。
如圖 11 所示,Flap 生態的特色能夠用 穩、快、準、狠 四個字來表達。
穩,意爲可靠的質量管理體系。在①IDE 開發中②提測階段③線上監控④降級容災,咱們都有對應的策略。其中②和③的基本是和 Native 相似的 PR 檢查、QA、日誌、上報之類的這裏就不作贅述了,下面主要提一下①和④。
IDE 語法檢測插件
這個功能的意義是儘早地將不支持的語法以編譯錯誤的方式暴露出來,以便同窗在開發期就能發現及時修改。 設想一下當你代碼寫完了,Code Review 也逃過了同窗的眼睛,PR 的 Dart 檢測也過了,開開心心下班了,忽然一個電話打來講發 Bundle 的時候錯了,有的語法 Flap 不支持,須要返工去改,此時你的心裏必定會「萬馬奔騰」。
因此,咱們將這種暫不支持的語法提早暴露,並推薦使用什麼方式代替,能夠有效的減小返工, 獲得一份知足 Dart 規範和 Flap 規範的代碼。一樣的 Lint 檢測規則後續也配置到了 PR 階段,若是真出現插件規則更新不及時場景,也會被攔在 PR 階段。
不過,目前 Flap 不支持的語法已經不多了,目前基本就是 await、as 和超過 2 個 with 等場景, 其中 await 和多個 with 的理論上也能支持,但會讓項目有較大的重構和多處的分別對待,不利於後期的維護,考慮到 await 徹底可使用 future.then 代替,因此這個語法就禁了。對於 mixin 的特性,在 Dart 側自己就是排列組合的關係。超過 2 個 with 會產生多個派生類,動態化的實現相似,因此爲了避免讓簡單問題複雜化,咱們也禁用了 2 個以上 with 的寫法,還有一些寫法上的限制,例如 import 不使用全路徑也會報錯。
目前開發中 Flap 動態化已經與 AOT 共用一份業務代碼了,爲了避免讓 Flap 的規則影響到項目中還未覆蓋到動態化的頁面,讓其滿屏報錯,咱們使用 @Flap 註解做爲是否開啓當前頁面的 Flap 規範檢測的開關。這也很好理解,當這個頁面內沒有 @Flap 時,確定是個 AOT 模塊則仍是默認的 Dart 檢測規則, 一旦加上了 @Flap('pageID'),說明此頁面會被動態發版,因此會自動開啓 Flap 檢測規則。
降級容災
Flap 接入了美團內部統一的動態化發佈平臺 DD,並利用 DD 平臺的能力實現了 App 版本、平臺類型、UUID、Flutter SDK 版本等細粒度的下發規則管控。業務方能夠根據實際狀況選擇不一樣的策略灰度發佈方案,若是發生了嚴重異常,Flap 也支持撤包操做。
某一個頁面加了標記支持了動態化以後,也會繼續進行 AOT 編譯過渡2個版本, 前置頁面點擊跳轉是跳 AOT 頁仍是跳 Flap 頁徹底由 URL 裏的參數控制,這個 URL 不是徹底由雲端下發的,是代碼中先寫上默認的 URL,若須要在配置平臺修改後,下發的配置信息會讓這個 URL 在路由側完成替換。即便配置平臺掛了,頂多喪失 URL 的替換能力而不是沒法前往落地頁。
對於 Flap 還有個更犀利的功能,在過渡期間(Flap 已經上線且 AOT 代碼還沒刪時),一旦 Flap 出現 Dart 異常, 當用戶退出頁面再進入時會自行進入該 pageID 下的 Flutter AOT 頁面,最大化下降對用戶的干擾。
快,意爲快速發版,快速更新。Flap 動態化改造使應用具有了分鐘級動態發版的能力,爲了更全面地釋放這個能力,客戶端業務迭代的流程也作了相應的調整。
當業務包發版上線,到了應用運行階段,Flap 主要面對的問題變成敏捷與質量的平衡,即:如何保證動態代碼可以儘快生效,同時又要保證加載性能和穩定性。
對於此問題,Flap 的解法是二級緩存與實時更新相結合,線上環境使用內存 + 磁盤二級緩存,進入頁面以後再預拉取更新包,平衡加載性能與更新實時性。而線下環境則強制加載遠程包,實現測試代碼的快速交付。
得益於這種機制,Flap 在線上能夠實現接近 Web 的觸達效率:應用會在啓動時和具體業務入口處發起更新請求,每當業務有動態發佈,新版本頁面便可在用戶下一次打開時觸達至用戶。在加載性能方面,二級緩存加持下的頁面加載時間僅爲數十毫秒,而遠程加載的時間也只有 1 秒左右。
細粒度動態化
準,指哪打哪,能夠頁面級動態化,也能夠局部 Widget 級別的細粒度動態化。事實上在 Flutter 的世界中,「頁面」自己也是一個 Widget,業務方在實際開發中,只須要增長一行註解,便可實現對應 Widget 或頁面的動態化。
@Flap('close_protect')
class CloseProtectWidget extends StatelessWidget {
// ...Widget 的 UI 和邏輯實現
}
複製代碼
Flap 打包發版時,解析引擎會從註解標記的 Widget 入手,遞歸解析全部依賴的文件,轉化成對應的 DSL 並打包。App 線上運行時,每一個動態化的頁面或組件都會按照註解的 FlapId,經過 FlapWidgetContainer 還原成對應的 UI。
實際調用時,只需傳入註解中標記的 FlapId,便可實現動態化區域或頁面的加載和渲染。
// 局部 Widget 級別的動態化,經過 FlapWidgetContainer 加載
Column(
children: <Widget>[
MyAOTWidget(), // 原生 Flutter AOT Widget
FlapWidgetContainer(widgetId: 'kangaroo_card'), // Flap widget
],
);
// 頁面級別的動態化,經過 MTFlutterRoute 路由跳轉:
RouteUtils.open('scheme://host/mtf?mtf_page=flap&flap_id=close_protect');
複製代碼
精準的 Debug 能力
在 Debug 階段加上一個註解 @Flap(‘pageId’),就會自動嘗試轉 DSL。若是該頁面很是獨立,且語法沒有太花哨,則直接就能看到轉換完成的字樣。這個就說明該頁面用到的語法既支持 Dart 又支持 Flap,不須要作任何修改。若是出現錯誤,則會在終端下精準打印出錯誤的位置。在此功能支持以前,基本都是「一崩就崩」到系統類的某某方法,開發同窗只能經過本身的經驗去堆棧中往上找。目前的精準 Debug 能力實現了轉換器、運行時 parser、運行時 evaluate 三個階段的全面覆蓋。
在轉換器階段的報錯位置信息可直接在 Exception 中得到 AST 對象的 lineinfo 進而獲取到列號行號信息。在 parser 與 evaluate 階段的錯誤定位是根據對核心方法的 trycatch 與設置通用 Exception 類型逐層上拋實現的。由於 DSL-JSON 會被壓縮且能夠 format,行號列號並沒有意義,因此在運行時階段的報錯全是精確到某 class 中的某 method。
狠,各類自動生成,實際轉換步驟操做方式簡單粗暴。Flap 在整個迭代流程環節都提供了便捷的自動化工具支撐。
imports 自動加載
基於 Flap 轉換一箇舊的 Flutter AOT 頁面到 Flap 頁面的操做是簡單粗暴的,加上註解,一行終端指令就能夠一把「梭」。但一個業務頁面爲了設計上的合理每每會分紅多個文件,若是有 10 個文件是否是要重複 10 遍這樣的工做?答案是否認的。Flap 不管是在 DSL 轉換器側,仍是在運行時加載 DSL,都會作到 imports 的遞歸加載。
IDE 語言檢測插件有一條限制是:import 必須使用 package 全路徑,不能只 import 一個類名。由於多文件須要導入的位置都是根據全路徑截取出的相對路徑來計算的。
Proxy-mirror 按需生成
前面介紹過 Proxy-Mirror 是外部符號轉內部符號的橋樑, 那麼具體 Dart 文件中哪些用到的類或方法須要內置 Proxy,而哪些類不須要呢?這個劃分的邊界就是,在轉換的代碼內可否看到此類或方法的聲明。系統方法的聲明確定不在業務文件裏,因此須要 Proxy。業務 Model 的聲明在「個人業務」文件中有,因此不須要 Proxy。代碼中使用到了官方 Pub 或是其餘業務線的 Pub,例如美團金融的 Pub 裏的方法,聲明不在「個人業務」文件裏,因此須要 Proxy。
在 Flutter AOT 遷移動態化初期,常常須要手動干預的問題是:項目中遇到 Proxy-Mirror 缺失會打斷轉換器, 須要手動補充後繼續進行轉換。
對於這種問題後期研發 Proxy 自動生成按需生成的工具, 主要原理是在預轉換階段,先掃描代碼的 AST Tree,壓平層級獲取全部的項目結構中 identifer 節點包裹的 Value,進行一系列斷定規則,而後基於reflectable 功能實現 Proxy 的自動生成。
發佈鏈路「一條龍」服務
通過不斷的提煉與簡化,目前開發者大能夠將注意力集中在開發階段,一旦代碼合入主幹,接下來就會有完整的 Flap 工程化發佈和託管系統協助開發者完成後續的打包、發佈、運維流程。前面介紹過的全部細節工做,都會由這些工具自動化完成,實現便捷發佈。Flap 也在路由層面對接了集團內通用的運維工具,開發者無須任何額外操做便可實現加載時間、FPS、異常率等基礎指標的監控。對於指標波動、異常升高等狀況,也會自動註冊報警項並關聯至當前的打包人。
業務落地只是咱們的目標之一,更重要的是在業務實踐過程當中,發現框架問題,完善各種語法特性支持,提升在複雜的混合場景下的兼容性,反哺促進框架的完善。不斷打磨的同時完善工做流,思考與沉澱最佳實踐,逐漸總結出合理的調試方案、操做步驟與協做方式,不斷提高開發效率與體驗。完善動態化基建及工具鏈建設,完成動態化流程的自動化與工程化,進一步下降轉換與開發成本。
對於 Flap 在業務中的實踐,主要有兩種應用場景。
場景1. 原有 Flutter 頁面,須要轉換成動態化頁面
設想一下,理想狀態下一個好的動態化框架應該是怎樣的?動態化框架將原有 Flutter 改寫成支持動態化的頁面?那加一個 @Flap 註解就行了。而後就能夠提交代碼,自動走工具鏈那一套。
目前,雖然沒有達到理想狀態,但咱們也在無限接近中,固然仍是要簡單地本地調試一下。基本都須要改個 URL 路由和 Mock 環境之類的步驟,咱們已經提供了模板的調試工程,支持一鍵對比 AOT 與動態化運行以後的差別,如圖 18 所示。基本就是加上註解,IDE插件會報錯哪些語法不支持,須要換一種寫法,而後跑一下就能夠,而後就提交代碼。
場景2. 直接使用 Flap 技術棧開發新頁面
從新開發場景很明顯比第一種要簡單,由於沒有歷史包袱。設想一下,好的動態化框架應該怎麼作?就是和 Flutter 的 AOT 開發使用一套相同的 IDE 環境,相同的開發模式,就是 IDE 會多報幾項語法錯誤罷了,開發時就能直接被提示到換一種寫法就行。寫完後加上註解,而後再提交代碼。
目前,咱們團隊已經把 Flutter 動態化能力在一些業務場景落地,固然業界也會有類似的或者不一樣的動態化方案。不管方案自己怎樣, 在落地時的步驟基本都大同小異,咱們也總結了一些經驗。
繞過問題並加以記錄
初期任何框架的能力都不是完美的,都會存在問題。業務方同窗遇到 Proxy 類缺失之類等比較簡單的問題能夠直接解決,運行時環境的深層問題、某些語法在複雜疊加場景下出現異常等等,通常會先嚐試用其餘的語法繞過,記錄文檔,而後同步到 Flap 團隊同窗進行解決。
定時補充 IDE Plugin Rules
對明確不支持的語法、關鍵字等添加到 IDE Plugin Rules 中,並提供了相關語法的替代方案,Rules 也會定時補充和刪減。
提早周知各方資源
包括確認好 Android 的上線節奏,QA 的測試節奏,以及周知PM動態化的覆蓋佔比。
關鍵權限收緊管理
相比於整理權限、灰度、降級、容災等線上的 SOP 和 FAQ,讓你們都學着操做, 直接指定 2~3 位超級管理員看上去更靠譜,線上環境由「老司機」把控更好。
業務應用涵蓋 App 一級頁在內的多個頁面,場景既有頁面動態化,也有局部動態化,經受住了一級、二級頁面的流量驗證。
圖 21 涉及到 PV 的地方打了馬賽克,Flap 團隊對包括 FPS、加載時間、Bundle 下載時長、渲染時長等 11 項指標進行了統計,能夠看到 FPS 平均是在 58 以上,渲染時長根據頁面複雜度的不一樣在 7~96ms 之間。
總的來講,各項指標表現均接近於 Flutter 原生性能。而且圖中的數據都還有可提高的空間,目前的平均值也受到了局部較差數值的影響,後續會根據不一樣的 TP 分位使用分層的優化方案。
咱們經過靜態生產 DSL+Runtime 解釋運行的思路,實現了動態下發與解釋的邏輯頁面一體化的 Flutter 動態化方案,建設了一套 Flap 生態體系,涵蓋了開發、發佈、測試、運維各階段。目前 Flap 已在美團多個業務場景落地,大大縮短了需求的發版路徑,加強了線上問題修復能力。Flap 的出現讓 Flutter 動態化和包大小這兩個短板獲得了必定程度的彌補,促進了 Flutter 生態的發展。此外,多個技術團隊對 Flap 表示出了極大的興趣,Flap 在更多場景的接入和共建也正在進行中。
將來咱們還會進一步完善複雜語法支持能力和生態建設,下降開發和轉換 Flap 的成本,提高開發體驗,爭取覆蓋更多業務場景,積極探索與業務方共建。而後基於大前端融合,探索打通其餘技術棧,基於Flap DSL 抹平終端差別的可能。
美團外賣長期招聘 Android、iOS、FE 高級/資深工程師和技術專家,歡迎加入外賣 App 你們庭。歡迎感興趣的同窗發送簡歷至:tech@meituan.com(郵件標題註明:美團外賣技術團隊)
閱讀更多技術文章,請掃碼關注微信公衆號-美團技術團隊!