釘釘 Flutter 跨四端方案設計與技術實踐 | Dutter

語言: CN / TW / HK

《Dutter 系列文章》將闡述釘釘基於 Flutter 構建的跨四端應用框架(代號 Dutter)的技術實踐與踩坑經驗,共分為上、下兩篇,本文為上篇,本週四將上線系列文章下篇《前車之鑑:聊聊釘釘 Flutter 落地桌面端踩過的“坑”》,歡迎追更 & 閲讀。

作者:劉太舉(駑良)

本文主要介紹釘釘基於 Flutter 構建的跨四端應用框架(代號 Dutter),內容主要包含方案設計、最佳實踐以及部分 FlutterEngine 層面的問題定位等。希望能通過本文的分享,為有類似訴求的團隊提供一定參考。

項目概述

1.1 何為 Dutter

Dutter 即 DingTalk Flutter,是釘釘內基於 Flutter 構建的跨四端研發框架。

Dutter 項目「起於 Flutter 但不止於 Flutter」。項目的主要目標是希望能夠藉助 Flutter 跨平台能力,在不降低用户體驗的前提下,提升釘釘端側研發效率,緩解釘釘端側研發資源不足、各端人力不平衡的問題。

1.2 目前進展

目前 Dutter 運行框架已完成釘釘四端集成,並完成了一系列共創業務的灰度和試點。現階段釘釘內基於 Flutter 研發業務有「日程簽到」「+面板」以及部分內部灰度業務:

項目背景

我們選擇 Flutter 並啟動 Dutter 項目主要有兩方面的考慮:

  1. 端側研發提效;
  2. 跟進 Flutter 技術。

下面我們針對這簡單展開做一下説明。

2.1 端側研發提效

隨着釘釘發展到第7個年頭,客户端側「業務需求」「研發資源」和「技術演進」這三者之間的矛盾愈發強烈:

  • 業務產品同學有很多優秀的想法,為將想法落地需要去尋找各端 TL 爭取資源,且因為研發資源緊張需反覆溝通其需求的業務價值;
  • 研發同學常常處於「1 vs N」的狀態,業務需求、穩定性保障、技術支持、BugFix等,日常工作時間基本趨於飽和;
  • 技術團隊不只是要滿足於現在,更要面向未來。在滿足日常業務迭代同時,我們還需要安排部分資源投入到滿足未來 3~5 年發展的技術項目上。

以上各點可彙總到一個問題:我們技術研發資源不足。為解決上述問題,有兩個途徑:1. 繼續擴大技術團隊規模;2. 提升團隊研發效率。

以目前釘釘端側將近150人的團隊規模來看,總體量並不算小,繼續擴招存在一定難度。既然團隊規模無法無限擴張,我們就需要在研發效率上挖掘提升空間:

  • 端側技術同學被分割到5個平台,分割之後每個平台上上人力並不算充足;
  • 不同平台下的同學技術棧上基本處於「隔絕」狀態,不同平台下的同學無法相互補位;
  • 任何業務需求需要需要 4+ 端以上的研發資源投入,任何一端人力欠缺都可能造成無法落地;
  • 一份邏輯多份實現,很難導致完全一致,時常會出現不同平台業務表現不一致的情況,返工對焦進一步影響效率;
  • 業務上線之後不同平台分別維護,在日常技術支持、BugFix 等場景下需要多份投入。

由此可見,如果我們能借助於跨平台技術,使技術同學可以通過「一份代碼實現覆蓋所有端」,將原來一個需求需要多個平台、多個同學分別做的事情收斂到 1~2 個同學上,即可極大的提高我們的研發效率。

2.2 跟進 Flutter 技術

釘釘內已經有「小程序」「H5」等跨端技術,我們需要提效是否可以直接使用現有技術棧來達成目標?對於釘釘端側團隊來説,選基於 Web 的方案來做跨平台理論上可行,但是實際很難達到預期效果。主要原因在於兩方面:

  1. 「小程序技術」是目前較為熱門跨端技術,其設計定位要滿足三方生態多樣性場景,其架構設計側重「大而全」,而非在單點上的反覆打磨。這與釘釘一方業務強調的「專而精」「追求極致」有出入;
  2. 對端側同學來説,前端開發模式上手門檻高、研發模式差異性較大。需要需要有一定的使用以及開發經驗積累才能具備較高的開發水平,也就是説前期需要有一定的「試錯空間」。以釘釘目前對線上質量的要求,這一點也是很難滿足的。

Flutter 作為最近幾年發展起來跨平台技術,不同於 Web 生態,其基於類 Native 的架構設計,選擇性放棄動態化、更關注於跨平台。在保證具有類似 Native 性能和體驗基礎之上,賦予開發者「一次開發多端構建運行」的能力。因此相比小程序技術,Flutter 更適合用於解決我們端側技術團隊的痛點。

除此以外,我們對國內跨平台技術進行摸底調研之後發現基於 Flutter 的跨平台項目後發優勢明顯,上限高、發展潛力大,更具長期投入價值。

在對業界跨平台方案的長期跟蹤中我們發現,「自繪引擎」是現階段一大熱點,而大多「自繪引擎」方案,是在 Flutter 項目開源並熱度上升之後開始啟動。這個時間點上的巧合並非偶然,我們通過下面這種圖來説明主流跨平台方案在技術實現上區別:

從上面這張圖我們可以看到,對於跨平台方案設計者來説,Flutter 項目最大的價值是:為生態提供了一個開源的、設計優秀的、兼容性優良的、性能優異的、邊界清晰的 自繪引擎。

基於這套開源的自繪引擎,具備技能能力的團隊只要稍加修改即可將其應用到自己的跨平台方案中以替換掉 Native 組件,複用 Flutter 具備的跨平台一致性能力,提升方案業務與技術價值。

對於釘釘來説,考慮到現階段我們在跨平台的投入和目標,還不是類似其它方案一樣推出自己的跨平台自繪引擎。但是從技術方向來看,選擇基於 Flutter 來做跨平台方案,一方面我們可以快速享受 Flutter 的技術紅利,在交付產物性能和質量上與其它主流方案保持一致;另外一方面我們也可以在這個過程中培養相關技術團隊,為後續更深層次的定製和改造做技術儲備。

方案設計

本章節會概要介紹釘釘 Dutter 跨端框架設計情況,並針對其中具有代表性的問題做一些補充説明。

3.1 總體設計

Dutter 核心模塊包含三大套件:

  1. Dutter Runtime;
  2. Dutter Dev Kit;
  3. 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 基礎之上封裝供業務使用的「響應」和「分發」接口。

這樣做主要有以下好處:

  1. 有利於性能穩定性,有限的的 Channel 可以降低通信異常概率、提升通信性能;
  2. 有利於管理,尤其是在「單引擎/多引擎」共存模式下,可以通過合理的封裝抹平底層差異。

上述兩點,尤其是第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 內存管理策略。首先我們需要我們需要準確定義核心原則:

  1. 分配與釋放同源:必須使用一套 alloc 與 free 算法,避免因為實現差異,導致內存分配釋放異常;
  2. 必須滿足「誰 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 業務層直接耦合:

模塊化之後並不僅僅只是對我們研發效能有提升,同時也帶來了顯著的業務和技術價值。比如:

  1. 為釘釘多版本提供了有力支撐,滿足「標準釘」「大客户釘」「專有釘」等多個版本共享代碼的訴求;
  2. 提供了良好的兼容性,通過對基礎模塊的靈活插拔,滿足 Dutter 框架在移動端和桌面端同架構的訴求;
  3. 提供了豐富的擴展性,例如我們在做 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 框架封裝的接口轉場,根據特定轉場模式執行轉場動作。

為了達到上述效果,我們在桌面端應用中主要解決了以下三問題:

  1. 桌面端集成模式問題;
  2. Widows 32位問題;
  3. 引擎架構兼容問題。

後面我們就針對上述問題分別做一下説明。

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,但上述方案最終還是因為各種各樣的問題無法落地。

雖然最終未能落地,但是在上述嘗試的過程中,我們瞭解到兩個非常重要的信息:

  1. DartVM 是可以運行在 Windows 32位設備上,但是僅支持以 JIT 模式加載 dart 代碼;
  2. Skia 可以編譯 Windows 32位產物。

在以上兩點的支持下,由釘釘 周鏞 同學最終探索出了編譯 Windows 32位 FlutterEngine 的方案,並通過 JIT 模式加載 Flutter 編譯產物,最終滿足在 Windows 端使用的訴求。

為了能夠在 Windows 平台使用 Flutter,剝離細節之後我們大致做了以下幾件事(詳細資料後面會有文章做專門分享):

  1. 修改 FlutterEngine 的構建腳本,使其能夠構建出 32位 的 flutter_windows.dll;
  2. 修改 flutter_tool 中 FlutterPlugin 編譯 gn 參數,使其構建 32位 的產檢產物;
  3. 將相關產物做安全混淆之後之後集成到釘釘客户端。

通過以上步驟我們即完成了在 Windows 32 位釘釘集成 Flutter 的主要工作,其後使用無論是 JIT 還是 AOT 在功能上並無本質區別,但是在性能上的差異較大。目前我們灰度過程中發現的主要問題有:

  1. 啟動速度慢:首頁加載時間在 2s 以上;
  2. 內存暫用高:每開闢一個 FlutterEngine 對象,大概需要消耗 70MB 左右的內存;
  3. 代碼運行效率低:此問題雖然絕大部分場景並不明顯,但是極端場景下還是會出現性能問題。

因此現階段我們採用的僅能算作一個刊用方案,後續我們仍需在此部分加大投入,爭取儘早讓一個完全的 Flutter 集成到釘釘 Windows 端。

4.3 引擎架構兼容問題

這個是我們在桌面端落地過程中遇到的第三個問題。由於在移動端我們使用的是基於 FlutterBoost 構建的單引擎架構,而桌面端則因為其特殊環境,只能使用多引擎架構:

因此對業務同學使用帶來一些問題,其中最嚴重的即為多引擎環境下導致的通訊阻塞。

現階段我們主要還是通過業務層兼容的方式來繞過:我們通過釘釘「消息總線」來支持多引擎環境下的通訊問題。但是長久來看我們還是需要有友好的支持多引擎,需要將目前移動端具備的 LightWeightEngine 能力擴展到桌面端,並在其基礎上進行擴展,打通 isolate 讓業務代碼完全共享內存。目前此方案整作為技術項目在 AliFlutter 項目組內推進中,期待早日達成既定目標!

總結

目前 Dutter 項目已經基本達成一階段目標,後續我們大致會在以下5個方面繼續投入:

  1. 基礎設施升級:移動端 FlutterEngine升級、flutter_boost 升級、 探索落地動態化方案等;
  2. 性能體驗精進:桌面端性能精進,最大化解決目前官方支持力度、基礎設施完備度、桌面端特性等原因造成的性能問題,爭取能對齊移動端水平;
  3. 研發套件完善:面向釘釘內提供一站式的研發環境,目前我們希望能在 AliBox 基礎上、面向釘釘四端研發場景,定向擴展部分以滿足釘釘內應用開發訴求;
  4. 穩定性增強:解決目前在桌面端、尤其是 Windows 端穩定性上存在的風險,滿足釘釘端側穩定性要求;
  5. 研發提效:擴大業務覆蓋面,釋放跨端宏利,進一步提升釘釘端側研發人效。

以上即為釘釘 Flutter 跨四端框架在應用設計上的一些分享,希望能為大家帶來一些幫助。

關注【阿里巴巴移動技術】,阿里前沿移動乾貨&實踐給你思考!