《Dutter 系列文章》將闡述釘釘基於 Flutter 構建的跨四端應用框架(代號 Dutter)的技術實踐與踩坑經驗,共分為上、下兩篇,本文為上篇,本週四將上線系列文章下篇《前車之鑑:聊聊釘釘 Flutter 落地桌面端踩過的“坑”》,歡迎追更 & 閱讀。
作者:劉太舉(駑良)
本文主要介紹釘釘基於 Flutter 構建的跨四端應用框架(代號 Dutter),內容主要包含方案設計、最佳實踐以及部分 FlutterEngine 層面的問題定位等。希望能通過本文的分享,為有類似訴求的團隊提供一定參考。
專案概述
1.1 何為 Dutter
Dutter 即 DingTalk Flutter,是釘釘內基於 Flutter 構建的跨四端研發框架。
Dutter 專案「起於 Flutter 但不止於 Flutter」。專案的主要目標是希望能夠藉助 Flutter 跨平臺能力,在不降低使用者體驗的前提下,提升釘釘端側研發效率,緩解釘釘端側研發資源不足、各端人力不平衡的問題。
1.2 目前進展
目前 Dutter 執行框架已完成釘釘四端整合,並完成了一系列共創業務的灰度和試點。現階段釘釘內基於 Flutter 研發業務有「日程簽到」「+皮膚」以及部分內部灰度業務:
專案背景
我們選擇 Flutter 並啟動 Dutter 專案主要有兩方面的考慮:
- 端側研發提效;
- 跟進 Flutter 技術。
下面我們針對這簡單展開做一下說明。
2.1 端側研發提效
隨著釘釘發展到第7個年頭,客戶端側「業務需求」「研發資源」和「技術演進」這三者之間的矛盾愈發強烈:
- 業務產品同學有很多優秀的想法,為將想法落地需要去尋找各端 TL 爭取資源,且因為研發資源緊張需反覆溝通其需求的業務價值;
- 研發同學常常處於「1 vs N」的狀態,業務需求、穩定性保障、技術支援、BugFix等,日常工作時間基本趨於飽和;
- 技術團隊不只是要滿足於現在,更要面向未來。在滿足日常業務迭代同時,我們還需要安排部分資源投入到滿足未來 3~5 年發展的技術專案上。
以上各點可彙總到一個問題:我們技術研發資源不足。為解決上述問題,有兩個途徑:1. 繼續擴大技術團隊規模;2. 提升團隊研發效率。
以目前釘釘端側將近150人的團隊規模來看,總體量並不算小,繼續擴招存在一定難度。既然團隊規模無法無限擴張,我們就需要在研發效率上挖掘提升空間:
- 端側技術同學被分割到5個平臺,分割之後每個平臺上上人力並不算充足;
- 不同平臺下的同學技術棧上基本處於「隔絕」狀態,不同平臺下的同學無法相互補位;
- 任何業務需求需要需要 4+ 端以上的研發資源投入,任何一端人力欠缺都可能造成無法落地;
- 一份邏輯多份實現,很難導致完全一致,時常會出現不同平臺業務表現不一致的情況,返工對焦進一步影響效率;
- 業務上線之後不同平臺分別維護,在日常技術支援、BugFix 等場景下需要多份投入。
由此可見,如果我們能借助於跨平臺技術,使技術同學可以通過「一份程式碼實現覆蓋所有端」,將原來一個需求需要多個平臺、多個同學分別做的事情收斂到 1~2 個同學上,即可極大的提高我們的研發效率。
2.2 跟進 Flutter 技術
釘釘內已經有「小程式」「H5」等跨端技術,我們需要提效是否可以直接使用現有技術棧來達成目標?對於釘釘端側團隊來說,選基於 Web 的方案來做跨平臺理論上可行,但是實際很難達到預期效果。主要原因在於兩方面:
- 「小程式技術」是目前較為熱門跨端技術,其設計定位要滿足三方生態多樣性場景,其架構設計側重「大而全」,而非在單點上的反覆打磨。這與釘釘一方業務強調的「專而精」「追求極致」有出入;
- 對端側同學來說,前端開發模式上手門檻高、研發模式差異性較大。需要需要有一定的使用以及開發經驗積累才能具備較高的開發水平,也就是說前期需要有一定的「試錯空間」。以釘釘目前對線上質量的要求,這一點也是很難滿足的。
Flutter 作為最近幾年發展起來跨平臺技術,不同於 Web 生態,其基於類 Native 的架構設計,選擇性放棄動態化、更關注於跨平臺。在保證具有類似 Native 效能和體驗基礎之上,賦予開發者「一次開發多端構建執行」的能力。因此相比小程式技術,Flutter 更適合用於解決我們端側技術團隊的痛點。
除此以外,我們對國內跨平臺技術進行摸底調研之後發現基於 Flutter 的跨平臺專案後發優勢明顯,上限高、發展潛力大,更具長期投入價值。
在對業界跨平臺方案的長期跟蹤中我們發現,「自繪引擎」是現階段一大熱點,而大多「自繪引擎」方案,是在 Flutter 專案開源並熱度上升之後開始啟動。這個時間點上的巧合並非偶然,我們通過下面這種圖來說明主流跨平臺方案在技術實現上區別:
從上面這張圖我們可以看到,對於跨平臺方案設計者來說,Flutter 專案最大的價值是:為生態提供了一個開源的、設計優秀的、相容性優良的、效能優異的、邊界清晰的 自繪引擎。
基於這套開源的自繪引擎,具備技能能力的團隊只要稍加修改即可將其應用到自己的跨平臺方案中以替換掉 Native 元件,複用 Flutter 具備的跨平臺一致效能力,提升方案業務與技術價值。
對於釘釘來說,考慮到現階段我們在跨平臺的投入和目標,還不是類似其它方案一樣推出自己的跨平臺自繪引擎。但是從技術方向來看,選擇基於 Flutter 來做跨平臺方案,一方面我們可以快速享受 Flutter 的技術紅利,在交付產物效能和質量上與其它主流方案保持一致;另外一方面我們也可以在這個過程中培養相關技術團隊,為後續更深層次的定製和改造做技術儲備。
方案設計
本章節會概要介紹釘釘 Dutter 跨端框架設計情況,並針對其中具有代表性的問題做一些補充說明。
3.1 總體設計
Dutter 核心模組包含三大套件:
- Dutter Runtime;
- Dutter Dev Kit;
- Dutter OPS Kit。
整體如下簡圖所示:
- Dutter Runtime: 基於 Flutter 構建的 Dutter 執行時環境,是 Dutter 最核心的部分。除去 Flutter 提供基礎功能以外,我們還提供了 容器化元件、API 外掛、業務模組化框架等功能。並且在於集團 AliFlutter 專案基礎上,進一步擴充套件了 Aion 動態化等功能。Dutter Runtime 也是我們專案執行到現在全力投入的部分;
- Dutter Dev Kit:即研發套件,主要目的是解決不同技術棧同學在 跨4+端 研發時的支撐和效率問題。目前投入相對有限,後續可與 釘釘研發平臺 合作整合;
- Dutter OPS Kit: 即運維套件,主要承載是 Dutter 產物釋出和運維相關功能,如大盤監控等。目前投入相對有限,後續可與 釘釘研發平臺 合作整合。
把上述簡圖展開,即可得到框架整體模組圖,大致如下:
從下向上以此為:
- 左下角部分為 「Dutter Runtime」 相關模組;
- 右下角為 「Dutter OPS Kit」相關模組;
- 右上角為「Dutter Dev Kit」相關模組;
- 左上角為業務部分。
3.2 資料通訊
資料通訊這塊主要就是指 Flutter 與平臺側兩種主要通訊方式:Channel 與 FFI。Channel 在 Flutter 應用中相對比較廣泛,絕大部分設計到 Flutter 與平臺通訊都是基於此模式展開,其優勢在於整合度高、封裝好使用簡單;劣勢主要在於通訊效率問題;FFI 在 Flutter 2.0 中已經作為正式特性推出,其最大特性在於同步呼叫、記憶體共享、執行效率高,但是在易用性、擴充套件性等方面還有一定提升空間。
Channel
關於 Channel,釘釘側使用相比官方文件並無本質差別,想分享的經驗在於 Channel 數量管理上。官方原生資料並未太多涉及 Channel 管理相關內容,以釘釘實際使用經驗來看,我們還是推薦大家在一方業務中,儘量將 Channel 收斂到 1~2 個做共享,並在共享 Chennel 基礎之上封裝供業務使用的「響應」和「分發」介面。
這樣做主要有以下好處:
- 有利於效能穩定性,有限的的 Channel 可以降低通訊異常概率、提升通訊效能;
- 有利於管理,尤其是在「單引擎/多引擎」共存模式下,可以通過合理的封裝抹平底層差異。
上述兩點,尤其是第2點對釘釘做移動端與桌面端相容有著巨大的意義。在「釘釘 Flutter 桌面端應用方案」中有說明,我們現在在移動端使用的是單引擎架構、但是桌面端部分採用的是多引擎架構。如果沒有對 Channel 做合理的封裝、讓業務同學直接面向 FlutterEngine 來做註冊與呼叫,則會極大的增加多引擎模式下的程式碼管理成本,並且會造成移動端和桌面端實現不一致。
我們現在的做法是將 FlutterEngine 與 Channel 封裝到 Dutter 框架內部,對上層介面暴露統一封裝之後的例項:DutterMethodChannel。對於業務層程式碼,已經無需感知底層架構是單引擎模式或者多引擎模式,僅需按照統一的規則和模式來註冊或者呼叫相關服務。通過此模式,在降低了業務使用複雜度的同時,也為底層框架設計帶來了極大的靈活性,為後續移動端切換多引擎方案提供了有力支撐。
FFI
FFI 已經在 Flutter 2.0 版本正式釋出,其相比 Channel 最大的優勢在於執行效率更高,更適合於對效能要求較高的場景。此章節不涉及具體 FFI 的使用方法,而是想為大家簡單分享在使用 FFI 時記憶體管理上所需注意的事項。
我們都知道,目前移動端開發(Java、OC、Swift)都有自動管理記憶體的的機制;Flutter 所使用的 dart 語言也有基於垃圾回收自動記憶體管理。各種語言在自己作用域中都可以按照各自規則來合理管理記憶體,保證記憶體空間合理穩定的應用。
但是 FFI 作為一種跨作用直調的方法,雖然基於記憶體共享的機制下簡化呼叫鏈路,但是對記憶體管理也提出了更高的要求。在這種模式下,如果不能很好的管理(開闢&釋放)記憶體空間,則有很大概率導致野指標或者記憶體洩漏問題。
在官方文件 Flutter FFI 與 Dart FFI 章節的介紹中,對記憶體管理上的說明較為有限。通過查閱相關介面資料可知,在 dart:ffi 中提供了手動管理記憶體的方式:
在此基礎之上我們即可定義 Dutter FFI 記憶體管理策略。首先我們需要我們需要準確定義核心原則:
- 分配與釋放同源:必須使用一套 alloc 與 free 演算法,避免因為實現差異,導致記憶體分配釋放異常;
- 必須滿足「誰 alloc 誰 free」的原則。
在 1 和 2 的基礎上,我們把 FFI 操作相關介面以及資料結構進行封裝,統一到「Dutter FFI Bridge」模組。
在對覆蓋面和複雜度充分考慮之後,Dutter FFI 介面中除預設基礎型別外,我們僅增加對 String 型別的支援。對於其它資料型別,業務方可以通過將其序列化的方式來進行傳遞。在傳遞過程中,對定長字串,可以直接通過「UTF-8 編碼的 char * 陣列」傳遞;如果是不定長字串(如呼叫返回值),則需要使用使用自定義資料結構 DTFUInt8String 傳遞。具體到實現:
1、為滿足「分配與釋放同源」原則,在 Dutter 中,我們選擇 dart:ffi 中的 allocate 和 free 方法作為統一分配和釋放實現。Dutter 框架會在啟動過程中做一次介面繫結,將我們自定義資料結構相關方法傳遞到 Native 側,Native 側所有 FFI 介面記憶體分配場景均通過繫結介面實現:
2、為滿足「誰 alloc 誰 free」原則,在 Dutter FFI 介面中,我們預設約定以下3原則。在此基礎上能夠保證堆記憶體的分配都在 DTFUInt8String 控制範圍內,只要處理好 DTFUInt8String 物件的生命週期,即可保證傳遞過程中記憶體管理的安全性:
a.介面設計時,對於需要不定長返回值的場景,使用 DTFUInt8String 來傳遞資料;
b.為提升傳遞效率,儘量以指標方式傳遞 DTFUInt8String ;
c.呼叫方負責建立以及釋放 DTFUInt8String 。
3.3 訊息匯流排
「訊息匯流排」是一個釘釘特色模組,我們主要是是為解決釘釘端側基於不同技術棧實現的業務通訊問題:比如一個基於 Flutter 實現的業務,希望通知一個基於小程式實現的頁面重新整理 UI,即可通過訊息匯流排來實現此功能:
訊息匯流排定位是一個輕量級「端」到「端」的超級通道,目標是讓業務具備跨執行環境無縫通訊的能力。在邏輯上包含「匯流排」「控制器」「註冊傳送」三大模組;在實現上通過「可持久化訊息」「管道分級」「許可權管控」等方式保證整體執行可靠、高效和安全。
3.4 模組化
因為釘釘端側業務特點,我們非常注重模組化建設。Flutter 業務採用的模組化方案發展自釘釘 Native 側模組化框架,我們在最初即堅持杜絕 Flutter 業務層直接耦合:
模組化之後並不僅僅只是對我們研發效能有提升,同時也帶來了顯著的業務和技術價值。比如:
- 為釘釘多版本提供了有力支撐,滿足「標準釘」「大客戶釘」「專有釘」等多個版本共享程式碼的訴求;
- 提供了良好的相容性,通過對基礎模組的靈活插拔,滿足 Dutter 框架在移動端和桌面端同架構的訴求;
- 提供了豐富的擴充套件性,例如我們在做 Flutter 動態化嘗試時,基於模組化能夠以較低成本對現有模組做動態化改造而不影響其它模組的穩定性。
3.5 容器化
容器化是支撐 Flutter 在釘釘內快速落地的有力保障。通過釘釘在 H5 和 小程式專案中沉澱的容器基礎,在 Flutter 場景我們繼續參考容器化思想,在設計和能力上快速對接。一方面得以快速複用現有沉澱的基礎設施;另外一方面降低業務開發上手複雜度,保證原容器常用能力在 Flutter 場景可以繼續使用,技術棧得以延續。
從發展時間軸來看,釘釘端側容器大致經歷過3個版本:
- v1.0 版本主要解決「有無」問題,定義容器相關核心概念;
- v2.0 版本在原基礎上抽象出「能力包」的概念,保證業務基礎能力可跨執行環境複用;
- v3.0 版本在 v2.0 基礎上進一步抽象出「執行時」和「擴充套件」,將核心實現下層為「容器底座」,三者之間弱耦合。
在目前容器架構基礎上,我們可以保證對未來新技術良好的相容性。在後續發展中如再次需要對接類似 Flutter 新技術棧時,可以按照現有標準快速打通,並在概念、能力、基礎設施上保證最大化複用。
3.6 元件庫
釘釘 Flutter 目前使用的元件庫有兩套:dingui_flutter 以及 dingtalk_uikit,其中 dingui_flutter 是我們現階段重點建設的部分,dingui_flutter 是按照釘釘視覺團隊提出的 DingUI 視覺規範實現的一套 Flutter 版本元件,目前核心元件可以做到四端相容:
dingui_flutter 目標是可貢獻給社群,但現階段因為穩定性、完善度等問題,暫時還在釘釘內部使用,後續發展成熟之後我們會將其儘早開源。
Flutter 桌面端
目前在釘釘桌面端中 Flutter 使用模式基本與移動端相同:Flutter 作為釘釘內的一個功能模組,客戶端主體仍以原 Native 實現為主。對於部分基於 Flutter 實現的業務,在啟動時通過 Dutter 框架封裝的介面轉場,根據特定轉場模式執行轉場動作。
為了達到上述效果,我們在桌面端應用中主要解決了以下三問題:
- 桌面端整合模式問題;
- Widows 32位問題;
- 引擎架構相容問題。
後面我們就針對上述問題分別做一下說明。
4.1 桌面端整合模式問題
Flutter 在桌面端目前還僅支援以 FlutterApp 的模式來使用,移動端廣泛使用的 FlutterModule 模式暫時還不支援。但期望通過 FlutterApp 來對現有客戶端做大範圍的改造,這既不合理也不現實。因此我們在桌面端落地 Flutter 遇到的第一個問題,即如何把 Flutter 作為一個模組整合到釘釘現有客戶端。
我們在對 Flutter 構建產物做分析的時候發現,其實無論是 FlutterApp 還是 FlutterModule,其核心產物差別並不大。以 iOS 端 FlutterModule 和 macOS 下 FlutterApp 來舉例,如下圖所示:
我們可以看到,對於 App.framework, Flutter.framework, Plugins.framework 這些核心模組,無論是 FlutterApp 還是 FlutterModule,其產物中都是包含的。主要差別在於 FlutterModule 中多了一個用於輔助外掛註冊的 FlutterPluginRegistrant.framework。幸運的是這部分實現並不複雜,我們可以很輕易的通過自定義工具鏈的方式來生成。
沿著這個思路,我們就可以梳理出 Flutter 桌面端整合方案:
通過 FlutterApp 來組織桌面端 Flutter 相關模組,在官方工具鏈基礎上做適當擴充套件。從原有構建產物中摘取作為模組化使用所需的部分,最後再補全部分用於外掛註冊所需的模板程式碼。最終產物整合到釘釘現有客戶端之後,使用上與其它二方庫並無本質差別,可參考現有 FlutterModule 的方法來使用。
最終流程如下:
Mac 和 Windows 端產物整合示意圖:
4.2 Widows 32位問題
Flutter 不支援 Windows 32位系統,應該是現階段阻礙 Flutter 在國內桌面端生態鋪開的核心阻礙之一。釘釘在解決此問題時,基本上嘗試了我們能想到的所有方案:從最初的雙程式,到中間的的整體升級64位,以及後面的 FFW,但上述方案最終還是因為各種各樣的問題無法落地。
雖然最終未能落地,但是在上述嘗試的過程中,我們瞭解到兩個非常重要的資訊:
- DartVM 是可以執行在 Windows 32位裝置上,但是僅支援以 JIT 模式載入 dart 程式碼;
- Skia 可以編譯 Windows 32位產物。
在以上兩點的支援下,由釘釘 周鏞 同學最終探索出了編譯 Windows 32位 FlutterEngine 的方案,並通過 JIT 模式載入 Flutter 編譯產物,最終滿足在 Windows 端使用的訴求。
為了能夠在 Windows 平臺使用 Flutter,剝離細節之後我們大致做了以下幾件事(詳細資料後面會有文章做專門分享):
- 修改 FlutterEngine 的構建指令碼,使其能夠構建出 32位 的 flutter_windows.dll;
- 修改 flutter_tool 中 FlutterPlugin 編譯 gn 引數,使其構建 32位 的產檢產物;
- 將相關產物做安全混淆之後之後整合到釘釘客戶端。
通過以上步驟我們即完成了在 Windows 32 位釘釘整合 Flutter 的主要工作,其後使用無論是 JIT 還是 AOT 在功能上並無本質區別,但是在效能上的差異較大。目前我們灰度過程中發現的主要問題有:
- 啟動速度慢:首頁載入時間在 2s 以上;
- 記憶體暫用高:每開闢一個 FlutterEngine 物件,大概需要消耗 70MB 左右的記憶體;
- 程式碼執行效率低:此問題雖然絕大部分場景並不明顯,但是極端場景下還是會出現效能問題。
因此現階段我們採用的僅能算作一個刊用方案,後續我們仍需在此部分加大投入,爭取儘早讓一個完全的 Flutter 整合到釘釘 Windows 端。
4.3 引擎架構相容問題
這個是我們在桌面端落地過程中遇到的第三個問題。由於在移動端我們使用的是基於 FlutterBoost 構建的單引擎架構,而桌面端則因為其特殊環境,只能使用多引擎架構:
因此對業務同學使用帶來一些問題,其中最嚴重的即為多引擎環境下導致的通訊阻塞。
現階段我們主要還是通過業務層相容的方式來繞過:我們通過釘釘「訊息匯流排」來支援多引擎環境下的通訊問題。但是長久來看我們還是需要有友好的支援多引擎,需要將目前移動端具備的 LightWeightEngine 能力擴充套件到桌面端,並在其基礎上進行擴充套件,打通 isolate 讓業務程式碼完全共享記憶體。目前此方案整作為技術專案在 AliFlutter 專案組內推進中,期待早日達成既定目標!
總結
目前 Dutter 專案已經基本達成一階段目標,後續我們大致會在以下5個方面繼續投入:
- 基礎設施升級:移動端 FlutterEngine升級、flutter_boost 升級、 探索落地動態化方案等;
- 效能體驗精進:桌面端效能精進,最大化解決目前官方支援力度、基礎設施完備度、桌面端特性等原因造成的效能問題,爭取能對齊移動端水平;
- 研發套件完善:面向釘釘內提供一站式的研發環境,目前我們希望能在 AliBox 基礎上、面向釘釘四端研發場景,定向擴充套件部分以滿足釘釘內應用開發訴求;
- 穩定性增強:解決目前在桌面端、尤其是 Windows 端穩定性上存在的風險,滿足釘釘端側穩定性要求;
- 研發提效:擴大業務覆蓋面,釋放跨端巨集利,進一步提升釘釘端側研發人效。
以上即為釘釘 Flutter 跨四端框架在應用設計上的一些分享,希望能為大家帶來一些幫助。
關注【阿里巴巴移動技術】,阿里前沿移動乾貨&實踐給你思考!