跨平臺開發框架是客戶端領域的經典課題,幾乎從操作系統誕生開始就是我們軟體從業者們的思考命題。為了促進 Flutter 在 4 個端的成熟,企業微信研發團隊也和 Google 團隊針對電腦端 Flutter 穩定版的落地做了多輪技術溝通。終於在近期的版本實現同一個功能跨平臺 4 端同步上線。企業微信每... ...
跨平臺開發框架是客戶端領域的經典課題,幾乎從操作系統誕生開始就是我們軟體從業者們的思考命題。為了促進 Flutter 在 4 個端的成熟,企業微信研發團隊也和 Google 團隊針對電腦端 Flutter 穩定版的落地做了多輪技術溝通。終於在近期的版本實現同一個功能跨平臺 4 端同步上線。企業微信每一個迭代都需要確保 iOS、Android、Windows、Mac 四個客戶端平臺的版本功能完全一致,版本發佈時間一致。這是非常大的挑戰。任何研發投入都是 X4 的,且由於系統差異,相同功能的研發周期和技術方案也會有明顯差異。我們前期實現了邏輯底層架構 4 端統一,但是 UI 層怎麼辦?迫切需要更優的跨平臺方案。但是要在歷史的 Native 代碼行數已經過千萬級的超大型軟體系統——企業微信上引入新的跨平臺框架何其困難。
經典案例:Flutter 實現跨平臺人事管理系統
人事管理是企業經營運作中“人財物事”的核心組成部分,而花名冊和人員入轉調離管理能力又是人事大板塊中最為基礎的部分,是企業的共性基礎需求,也是後續更多 HR 應用的底層系統支撐能力。整體需求邏輯複雜,涉及新的交互頁面上百個,在企業微信框架內補齊人事板塊需要 win、mac、ios、android 四個平臺都支持。企業微信相關產研團隊面臨極大挑戰如何在較小人力投入下短時間內能夠順利迭代出一套完善穩定的人事系統,而此時研發團隊持續兩年迭代沉澱的 Flutter 跨平臺 ui 融合框架起到關鍵作用,全平臺技術棧高度一體化,研發人效上比傳統分平臺開發模式。
提升 1 倍以上,後期產品、設計、測試驗收協同成本也降低 50%以上。下麵我們會詳細為大家介紹企業微信在跨平臺 ui 道路上的建設歷程。
一、項目背景
經過 Tob 業務的高速發展,市場的競爭也愈發激烈,包含了企業微信、釘釘、飛書等各類的辦公軟體,為了搶占 B 端市場,滿足企業用戶的業務需求,功能數量增長非常快。企業微信目前已經深耕眾多行業,並且有 android/ios/mac/ windows/web 五大開發平臺,使企業微信迅速發展成為了一個超大型應用。
我們也遇到了超大型 App 通常會存在的問題,每次版本迭代都需要五端進行同步迭代發版,各端人力開發成本急劇上升。為了提高開發效率,企業微信在跨平臺上也一直有做一些嘗試:
底層跨平臺開發架構
企業微信客戶端的設計架構採用的是四端 C++ 底層跨平臺開發架構,將 db、網路、日誌等能力通過 C++來實現,各端可以復用邏輯層介面。雖然邏輯層統一實現了,但是 UI 層仍然是由各平臺獨立開發,因此我們也在繼續探索 UI 跨平臺的方案。
小程式 UI/H5 跨平臺
為了提高業務上層的開發效率以及與微信互通的能力,企業微信在早期就已經接入了小程式和 H5 的方案,但是小程式和 H5 的方案跟原生體驗會有比較大的差別,無法滿足所有的業務場景。
在跨平臺的選型上,Flutter 在繪製上能夠保持各端的一致性,並且擁有出色的性能,Dart 對於原生開發的同學在技術棧上也會更加友好。在綜合對比了主流的跨平臺框架後,我們決定將 Flutter 作為跨端開發的主要框架之一。
Flutter 移動端跨平臺
2020 年開始企業微信就已經在探索跨平臺開發框架,將 Flutter 作為企業微信移動端主要的 UI 跨平臺開發框架之一。通過一年多的基礎建設和業務上的開發,我們 Flutter 移動端建設也達到了工程化的架構,在架構上我們經歷了原始的模塊化到插件化的迭代,在跨平臺的體驗上,組件以及動畫逐漸對齊了原生的體驗效果。
在移動端在業務開發中,得益於 Flutter 強大的跨平臺能力,為我們整個項目團隊帶來了一定的效率提升,所以我們希望將 Flutter 這項跨平臺技術推動到整個客戶端中心,來解決桌面端的人力緊張等問題。
Flutter 四端跨平臺
在桌面端的平臺上也是通過四端跨平臺底層來進行開發的,四端的邏輯層能夠得到了很好的復用,但是 Win/MAC 在開發原生應用的時候仍然是各平臺來進行獨立開發的,MAC 因為用戶量較少等原因,人力相對 Win 來說比較緊張,人力上的不足就會導致 MAC 的需求很難跟上版本的節奏,但是依然有客戶對 MAC 的功能有訴求的。
所以我們希望能夠通過跨平臺的能力來解決這部分的不足。企業微信在桌面端的跨平臺建設上就已經支持小程式/electron 框架,小程式因為體驗上跟原生應用有很大的差別、electron 無法適用於四端的跨平臺開發。因此都無法滿足我們日常需求開發。
在 2021 年的時候,我們就已經開始在桌面端接入 Flutter,期間針對多項難點問題持續攻堅,直到 Flutter 3.0 之後,Flutter 全平臺進入了 stable,我們也逐步完善了 Flutter 跨四端的框架能力,企業微信四端統一技術棧的設想也正式走上軌道。
二、Flutter 跨四端的融合工程架構與挑戰
2.1 整體架構圖
企業微信 Flutter 工程的整體架構圖如下,從下往上:
-
企業微信四端原生應用:原生應用是企業微信 Flutter 跨平臺能力的基石,在底層上主要包含了 C++ 四端跨平臺邏輯處理能力,是 Flutter 處理網路/DB/線程調度/Service 的核心,在上層中包含了 Flutter 的容器,承載著 Flutter 運行以及與原生之間的交互。配套的還有跨平臺相關的 CI 打包。 -
Flutter 應用部署方式:企業微信 Flutter 跨平臺能力可以通過源碼集成部署到原生的應用中,也可以通過 application 的方式獨立運行。 -
跨語言通信層:Flutter 作為上層業務開發,需要與原生進行通信,在通信層,主要包含了通過 dart::ffi 直接調用 c++ 底層能力;通過 channel 調用原生的 api 介面,以及通過 socket 的方式對原生應用的介面進行單元測試。 -
四端統一跨平臺:跨平臺層由 Flutter 統一四端開發,包含了 Flutter 工程化開發的腳手架。並且代碼模塊化,由基礎組件提供四端的路由/組件/RPC 的等能力。在動態化能力上支持 liteapp 的動態化能力,這一層是 Flutter 開發主要核心部分。
2.2 四端跨平臺的困難與挑戰
在接入企業微信的過程中,需要攻剋很多難點問題:
1)四端跨平臺混合工程,整個企業微信客戶端包含跨平臺部分,擁有著千萬行的代碼量級,業務模塊上百個,涉及界面上千個,並且跨多個團隊協作開發,環境依賴複雜,需要保證不影響現有的架構下完成接入 Flutter 跨平臺的開發能力。
2)多端跨語言的調用,Flutter 通過 dart 來進行開發,避免不了與原生平臺進行通信,涉及到終端 dart/kotlin/objectC/c++ 等編程語言,需要有一套通用高性能的跨語言介面調用方案去解決四端的跨語言通信問題。
3)桌面端穩定性治理,Flutter 桌面端仍然處理早期的穩定版本,在桌面端落地的過程中,會遇到各式各樣的坑,因此想要在桌面端落地,需要自主分析問題以及修改引擎來修複這些坑。
4)保障跨平臺的用戶體驗,Flutter 通過 skia 渲染來達到跨平臺開發的一致性,但是也因此失去了一些平臺的 UI 組件特性,為了保障產品體驗,需要在 Flutter 上持續完善原生組件能力。
三、企業微信超大型原生工程嵌入 Flutter 應用
整個企業微信客戶端包含跨平臺部分,代碼量級超 1500 萬行。客戶端本地模塊和業務的數量達到了上百個,相關頁面超過 2000 個。企業微信接入 Flutter 之後,會影響到各端的編譯流程和依賴結構,但是要保障現有的開發模式不受影響,並且提供一套完整的自動化以及容器化的方案。同時面對各端的複雜編譯環境(gradle、bazel、xcode、cmake)同時保障 Flutter 環境的高效開發以及編譯的穩定性,面對這個巨大體量的工程,為我們接入 Flutter 帶來了一定的困難。
企業微信 Flutter 研發流程圖
Flutter 在移動端提供了 add2app 的方式接入到原生的項目當中,並且提供了 Flutter module 的工程結構,可以很方便地將 Flutter 的 module 接入到原生工程進行打包和調試。
但是在桌面端,官方目前還沒有提供混合工程的接入方案,因此我們需要在打包編譯的時候做一些額外的配置,以支持混合工程的開發的目的。
雖然桌面端沒有提供 add2app 的命令直接輸出混合開發的產物,但是我們可以通過 Flutter application 工程,藉助 Flutter build 相關的命令進行應用程式的打包,不同平臺的主要產物如下:
Win:
Mac:
App.framework/app.so 為 dart 的 aot 編譯產物,主要包含了項目的所有 dart 源碼。
FlutterMacOS.framework/flutter_windows.dll 為 Flutter engine 層和 Embedder 平臺嵌入層的代碼, engine 主要是用來驅動 Flutter 運行的,平臺的嵌入層是用於呈現所有 Flutter 內容的原生系統應用,它充當著宿主操作系統和 Flutter 之間的粘合劑的角色,主要是原生平臺的代碼。
這兩個文件就是原生工程主要依賴的產物,另外一些資源/插件相關的文件也需要,需要將這些產物混合到原生工程裡面併進行引用和編譯,然後通過 FlutterMacOS.framework/flutter_windows.dll 引入的 sdk 來調用原生平臺的代碼啟動 Flutter 頁面。
四、四端跨語言通信建設
對於 Flutter 的通信主要分為以下兩部分:
1: 前面提到, 企業微信是通過 C++ 來實現邏輯層的跨平臺,企業微信作為原生與 Flutter 跨平臺融合工程,為了提高開發的效率,以及與原生平臺的相容,避免不了需要復用底層 C++已有的能力,並且由於調用量巨大,Flutter 上要能夠通過高性能的通道直接調用到 C++層。
2: Flutter 上層的開發避免不了使用原生已有的介面,需要與宿主工程的介面打通,而宿主工程又包含 Android/iOS/MAC/Windows 四大平臺,並且上層的介面使用的語言各不一樣,因此需要考慮一套多端跨語言的通信建設。
1: 如何高效復用 C++統一跨平臺能力
dart 2.15 之後提供了 dart::ffi 的方式調用 c/c++ ,在項目的實際開發過程中,我們也遇到一些大型工程下 ffi 的使用問題:
1: dart 調用 c++操作步驟繁瑣, 介面維護和約束困難
2: c++調用 dart 方法只支持靜態方法或者頂層函數
3: dart 上開放了指針的分配和釋放,調用 c++之後記憶體管理混亂,容易造成記憶體泄漏
4: 如果出現介面綁定不匹配的情況或者 so 忘記更新,會導致全局的異常,影響正常開發流程
為瞭解決以上的問題,我們參考 grpc 的設計流程,設計了一套跨語言的 rpc 調用模型,通過 protoc 插件來自動生成 dart client 端 和 c++ server 端的介面,簡化了開發的成本,並且對介面進行了一定的約束。
另外調用 c++的介面不再受限於靜態方法或者頂層函數,開發調用 c++的介面就跟調用本地的 dart 介面是一樣的。
在 rpc 的調用過程中,通過將 rpc 的 transport 層,替換成各個語言之間的調用通道,在 Flutter 上就是利用單個 ffi 介面進行請求的收發,從而達到跨語言調用的目的,在框架內部進行線程以及記憶體的維護與管理。
調用的流程如下:
final GovernRpcServiceApi service = GovernRpcServiceApi(CppChannel());
final req = GetGovernMyReportListReq()..limit = 10;
final result = await service.getGovernMyReportListFromServer(req);
集成部署的情況下,能夠通過 ffi 直接調用到底層,各端能夠很好復用已有的能力。但是在 win 上,由於是企業微信採用的是多進程的架構,需要 Flutter 應用進行獨立部署,與企業微信宿主之間的通信需要經過企業微信的 ipc 通道,如果是獨立部署的 Flutter 應用,在 transport 層,將數據通道從 ffi 轉換為 ipc 的通道,以此來達到調用企業微信跨平臺底層的能力。雖然對於不同的部署方式 transport 的傳輸通道會有區別,但是對於開發者來說,調用確是透明的,開發不需要關心當前走的是 ffi 還是 ipc,也不需要關心當前 Flutter 應用的打包以及運行方式。
2: 四端跨語言介面調用方案
Flutter 提供了 channel 的方式進行原生平臺介面的調用,如果只是依靠 channel 的方式來進行與原生平臺通信,介面的維護就會變得非常麻煩,由於平臺上的擴展,各端溝通成本也會提高,channel 不適合於大型工程上的開發。
官方推薦通過 pigeon 的方案來自動化生成介面,但是 pigeon 早期尚未支持桌面端,因此不適用於企業微信的業務開發,另一方面,pigeon 的介面依然是通過 channel 來維護的,企業微信的介面需要考慮服務發現、動態註冊、安全校驗等能力,通過 pigeon 的維護方式不便於處理這些場景。
因此,在 dart 調用 c++的基礎上,我們繼續擴展了 dart 調用其他平臺介面的能力,並且實現了一套 channel 的自動化框架:rpc-channel,和 pigeon 的主要區別如下:
我們通過 protobuf 來統一各個平臺的介面,並且實現 protoc plugin 為我們生成各個平臺的介面代碼,再由各端實現 grpc server 端的分發以及處理請求的能力。native 平臺作為 server 端只需要實現對應的介面即可。
在 Flutter 端我們依然通過 grpc 的介面來進行調用,只不過調用所需要的 transport 通道變成了 platformChannel 的方式來調用,通過這種方式,我們收攏了所有的 channel 調用介面,並且都通過單個 channel 來做數據分發,單個 channel 的方案,更加方便於我們對所有的 channel 介面進行統一的管理,做服務發現、安全校驗、統一日誌等邏輯。
調用的方式如下:
//由CppChannel 變成了PlatformChannel,通道即發生了變化
final GovernRpcServiceApi service = GovernRpcServiceApi(PlatformChannel());
final req = GetGovernMyReportListReq()..limit = 10;
final result = await service.getGovernMyReportListFromServer(req);
五、融合工程遇到困難與挑戰
1: windows 針對 cpp/channel 跨進程通信
在 windows 上,為了減少與主工程的耦合性,我們將 Flutter 插件作為獨立的進程運行,跟其他端不一樣的是, Flutter 與 原生工程的通信方式會有一些改變,包括我們的 channel 以及 底層的調用,因此我們在企業微信的 ipc 通信的基礎上,實現了 channel/dart2cpp 的通信,具體的調用流程如下:
win 由於是獨立進程,dart2cpp 以及 channel 的調用都是在獨立進程下的,因此沒辦法直接調用到宿主的工程,要藉助於企業微信的 ipc 通信,從上面介紹過 channel 以及 ffi 介面,由於我們對 channel 以及 dart2cpp 的介面進行統一的管理,所有的事件都會經過 stub 類來進行集中處理裝包併進行數據的傳遞。我們在 stub 裡面,額外對 win 進行了適配,如果是 win 會將請求通過 ipc 轉發到宿主工程上,而不是直接調用分進程的介面,調用的過程如下。
2: windows 32 位編譯問題以及處理方案
在 Flutter 在 3.0 之後在 engine 層面提供了 32 位 windows 的編譯選項,但是由於 dart 的限制,也是只允許編譯 jit 的模式,並且 Flutter 層面的編譯尚未支持,企業微信在探索 jit 的模式是在 3.0 之前,比官方更早地完成了 32 位 jit 的適配,並且包含了 Flutter 32 位 windows 編譯選項的改造:
1: 由於 3.0 之後已經支持 32 位的編譯,Flutter engine 可以編譯 windows jit-release 產物,相關的 gn 命令以及 build 如下:
python .\flutter\tools\gn --target-os=win --windows-cpu=x86 --runtime-mode=jit_release --no-goma
ninja -C .\out\win_jit_release_x86
2: Flutter 編譯改造 Flutter 編譯 windows 主要是通過 Flutter build windows 相關的命令,預設是編譯 64 位的包,並且沒有相關的參數支持 32 位的編譯,編譯完 32 engine 之後,需要改造 flutter 倉庫相關的代碼,適配 32 位的 windows。
Flutter build 相關的命令主要是由 packages/flutter_tools/bin/flutter_tools.dart 經過 dart 編譯得來的,修改 flutter_tools 的代碼之後,刪除 bin/cache/flutter_tools.snapshot,執行 flutter doctor 命令重新編譯 flutter_tools,即可更新 flutter build 命令。這裡我們根據 Flutter build windows 的流程, 增加 jit-release 的編譯模式。
Flutter build windows 相關的流程核心主要是從 BuildWindowsCommand 開始。
主要要修改的是:_runCmakeGeneration:主要是通過 cmake 命令編譯 win 產物,需要將 targetPlatform 作為參數傳進來,如果是 x86 架構,cmake 命令後面要加上 Win32 參數,以便構造 Win32 的產物。
3: Win7 特定版本打開 Flutter 黑屏的問題
線上上的投訴中,有部分 win7 設備的用戶反饋黑屏的問題,經過分析黑屏的用戶都是在 win7 某一個特定的小版本上,Flutter 上也有相關的 issue 在跟進:
https://github.com/flutter/flutter/issues/89583
目前 issue 上提供的解決辦法是安裝.net 庫解決,但是並沒有定位的真正的原因,企業微信通過分析 DirectX 相關的庫,發現黑屏的用戶主要是缺少 d3dcompiler_47.dll 庫引起的,通過內置這個庫就可以解決這個問題,而不用引導用戶安裝.net。
4: Win 分進程視窗無法前置
問題:當點擊 Flutter 的區域時,無法將企業微信視窗前置。
原因:由於 windows 採用了多進程模型,企業微信和 Flutter 不在同一個進程中,點擊 Flutter 區域只是激活了 Flutter 進程的視窗,企微對應的視窗沒有激活。
解決方案:在 Flutter 視窗收到滑鼠激活消息時(WM_MOUSEACTIVATE),將該視窗對應的 Ancestor 視窗前置。
5: Windows7 搜狗輸入法錯位問題
錯誤現象:
輸入 nihao,按 1 確認輸入你好,再繼續輸入其他文字,會把你好給刪掉。
出錯的跟本原因:
搜狗 在 win7(win7 SP1)系統上輸入法確認輸入的時候,會同時發 GCS_COMPSTR 和 GCS_RESULTSTR 兩個輸入法消息,在 win10 上是只有 GCS_RESULTSTR 一個的,這種消息的錯亂直接導致 Flutter 在處理 composing 文字的時候出現反饋中的問題。
錯誤分析:
從收到的輸入法消息上看,在確認輸入的時候多了一個 GCS_COMPSTR commit 的消息,這個消息是個空的。
commit 為空消息會把當前正在輸入的內容清空。Engine 層收到這個空的消息之後,會把 engine 層把正在輸入的文字全部清掉,然後通過 channel 通知 Flutter,Flutter 收到消息之後,發如果個空的消息,就會通過 channel 通知 engine setText 為空(只有空文本這個時機才會觸發 flutter->engine)。
問題在於 engine 通知 Flutter 的過程是個非同步的,在通知 Flutter 之後,緊接著 RESULTR 事件來了,RESULT 事件將 engine 層的 text 設為“你好”,改完了之後,flutter 通知 engine 上一次處理 GCS_COMPSTR 事件來了,又把 engine 層的文本給清空,後面的事件中 Flutter 都不會通知 engine,所以在下一次輸入的時候,engine 層認為輸入框中正在輸入的文字是空的。
引發出來的文字錯亂問題:
前面的文字被莫名其妙刪除之後,再輸入文字,會出現重覆的文本。
錯誤原因:
在 Flutter 通知 engine 更新 text 為空的時候,導致 Flutter 記錄 composingRange 的數據出錯, range 變成了(0,0), range 出錯直接導致 UpdateComposingText 的過程中:
text*.replace(composing_range*.start(), composingrange.length(), text)
replace 就會在原有文本的 0 坐標下,替換成新的 text,但是由於 length 是 0,所以就出現了重覆的情況。
解決辦法:
調換處理 GCS_COMPSTR 和 GCS_RESULTSTR 的邏輯,讓 GCS_COMPSTR 空的消息最後處理。這種解決辦法的核心在於,engine 在處理 GCS_RESULTSTR 消息的時候,會有一個 CommitComposing 的邏輯處理,表示結束掉當前的 composing 狀態,當結束 composing 之後,收到 GCS_COMPSTR 為空的時候,因為 composing 的文字為空了,再去處理 composing 中的文字已經沒有意義了。
6: Mac 記憶體泄漏
記憶體泄漏問題
1: 由於 Flutter 目前還沒有考慮到混合工程的結構,因此在接入到企業微信之後,每次進出 Flutter 應用,發現對應的 FlutterEngine 都沒有被釋放,這種問題在獨立應用中是沒有的。因此我們開始分析並且解決了記憶體泄漏相關的問題。
泄露 FlutterEngine 的主要原因:FlutterEngine 中通過弱引用持有 viewController,當 viewController 退出的時候,會觸發 engine.setViewController=nil,但是 viewController 因為弱引用的關係,已經變成 nil 了,導致後面 shutdownEngine 相關的邏輯都不會執行。
解決的辦法:修改 Flutter Engine 的實現, engine.setViewController=nil 的情況正常觸發後面的流程。
- (void)setViewController:(FlutterViewController*)controller {
if (_viewController != controller || controller == nil) {
//正常觸發後面的邏輯
}
}
2: 退出 Flutter 頁面, FlutterEmbedderKeyResponder 和 FlutterKeyboardManager 記憶體泄漏。
原因:FlutterEmbedderKeyResponder 通過 block 強引用了 FlutterKeyboardManager,而 FlutterKeyboardManager 又通過 addPrimaryResponder 引用 FlutterEmbedderKeyResponder 從而造成迴圈引用。
解決辦法:修改 FlutterKeyboardManager.mm 的代碼,通過弱引用來解除這個迴圈引用的關係。
低版本 OpenGL crash 析構引起的 crash
crash 的主要原因:為瞭解決記憶體泄漏,Flutter 在退出的時候完全釋放到 Flutter 相關的引用,從而導致觸發了 FlutterOpenGLRenderer 釋放 OpenGLContext,在 10.13 的或者更低的系統上,openGLContext 在析構的時候會出現了 crash。解決辦法:在 FlutterOpenGLRenderer 中,讓 openGLContext 不要釋放,來規避這個 crash。
六、UI 體驗優化以及調試工具
1: 四端 UI 組件庫
在四端的 ui 組件上,我們分為了移動端和桌面端兩套 UI 組件,在組件中我們除了完善企業微信現有組件外,對各端常遇到的體驗問題也做了改進。
移動端組件體驗優化
IOS 原生容器與 Flutter 容器切換導航欄優化
背景:
企業微信採用的是單容器多 Flutter 頁面的混合棧方式,Flutter 內部通過 CupertinoNavigationBar 來模擬 IOS 導航欄的切換效果。但是 Flutter 的導航欄採用的是自渲染的方式,ios 的導航欄在切換到 Flutter 容器的時候,由於是兩個不同的導航欄,導致原生導航欄的動畫無法正常銜接上,就會出現兩個導航欄同時位移的動畫,如圖所示:
為瞭解決以上的問題我們探索了兩種方案:
1: Flutter 單頁面單容器的方案,導航欄由原生來渲染,頁面的切換動畫完全由原生來控制。
2: 原生切換到 Flutter 容器的時候,先展示 IOS 的導航欄,動畫消失後再把 IOS 的導航欄隱藏掉。第一種方案的好處是達到原生一致的效果,但是對於 Flutter 開發來說,導航欄的自定義性就會變得很差,如果要渲染 icon,響應點擊事件就會變得非常麻煩,而且與導航欄相關的交互情況要考慮得也非常多,對現有的混合棧結構的改動非常大。
因此我們採用的是第二種方案,在容器和 Flutter 上實現了一套帶原生動畫的導航欄, 在進入 Flutter 容器動畫的過程中,會先展示 ios 原生的導航欄,flutter 在導航欄渲染之後,會通過截圖的方式將導航欄上的元素截給 native,native 通過圖片的方式在導航欄上渲染 flutter 的元素,動畫完成的過程之後,再隱藏掉原生容器的導航欄。
實現上述技術點的關鍵在於 Flutter 導航欄要做到:
1: IOS 的 NavigationBar 在頁面初始化的時候就必須得準備好顏色和佈局,後續動畫的過程中不能對顏色和佈局進行變更,在進入 Flutter 頁面之前,先讀配置文件或者由代碼指定導航欄樣式。另外由於 NavigationBar 的元素在動畫的過程中也是不能進行變更的,我們利用 ImageView 提前在 NavigationBar 上占位,動畫的過程中,只更新 ImageView 的內容。
2: Flutter 導航欄渲染出來的效果和 IOS 導航欄的渲染效果必須是完全一致的,這樣在原生的導航欄消失之後才不會出現閃動的情況,因此需要我們對 Flutter 上的導航欄進行一些改造,對齊 IOS 的導航欄規範。
3: 需要對 Flutter 導航欄上的元素進行截圖,並且遇到導航欄元素刷新的情況,截圖有可能是多次的,如果不通過截圖的方式,遇到 icon 或者 中英文、大小字體的情況,Flutter 的導航欄是很難對齊原生的,這裡用圖片進行傳輸,實測下來也並不會影響到實際體驗。實現之後整體的效果如下,切換到 Flutter 容器跟其他原生頁面是完全一致的體驗。
IOS 導航欄內部切換效果優化
在實現完容器直接切換的動畫之後,我們面臨第二個問題,內部的導航欄動畫優化,如果是兩個相同背景顏色的導航欄之間的切換,Flutter 幾乎是達到了原生一致的效果,但是如果兩個導航欄上顏色不一致,企業微信上會有更加複雜的動畫:
而 Flutter 對不同顏色的導航欄之間的切換採用的是漸變的方案,但是設計希望對齊企業微信以及微信原生的表現,頁面和導航欄都有整體的拖動效果,但是導航欄的元素是不會產生較大的變化。
解決辦法:我們改造了 CupertinoNavigationBar 的動畫,CupertinoNavigationBar 在模擬器 IOS 動畫的過程中,其實是利用了 Hero 相關的特性,通過 HeroFlightShuttleBuilder 了完全重寫了 Hero 動畫。
動畫整體的思路在於,去掉漸變相關的動畫,並且通過 Stack 的組件,在原有導航欄動畫的基礎上,新增與當前導航欄顏色一致的 Container, 利用 ModalRoute.of(context)的方式,拿到頁面的轉場動畫(這裡與 hero 的動畫是有區別的),最後對 Conatiner 做 SlideTransition 轉場動畫。
額外需要註意的是,用戶側滑返回跟點擊返回的動畫是有區別的,需要做一些判斷:實現的效果如下:
以上兩個是 IOS 遇到的體驗影響比較大的問題,還有其他一些對齊 IOS 點擊態效果、文本輸入框下劃線對齊 IOS 背景色、側滑返回快速點擊無響應等體驗問題我們也都在組件中完善並且解決了,並且提供了 demo 的獨立程式。
桌面端組件完善
在桌面端接入 Flutter 之後,Flutter 目前對桌面端的組件完善程度並不夠,我們也在完善桌面端相關的 UI 組件,並且提取了一些桌面端組件常見的問題:
1: Flutter 提供了 MouseRegion 來實現 Hover 態,開發在實現組件的時候需要關註桌面端組件與 Hover 的操作,這種表現在移動端是沒有的。
2: 對鍵盤事件的處理,比如列表需要支持按住某個按鍵切換為橫向滾動,實現上可以利用 Listener 監聽滑鼠的滾動事件,並且通過 pointerSignalResolver 做相應的攔截,攔截之後,將 controller jump 到橫向指定的 offset。
下麵是 Flutter 桌面端的組件庫:
2: Flutter 視窗控制項化
因為引入了分進程,Flutter 與企業微信不在同一進程中,通過分進程打開的 Flutter 頁面屬於分進程的一個獨立視窗。視窗的生命周期和樣式不在企微中管理,這種方式很難適配複雜的業務場景。相當於每個使用了 Flutter 的業務都要關心 Flutter 視窗的樣式,在不滿足業務場景時,要修改分進程代碼支持。對業務方不友好且很難維護。
改進方案如下:
-
將 FlutterWindow 作為子視窗嵌入企業微信的 HostWindow 中 -
通過 FlutterConatinerView 控制 HostWindow 的顯示區域
通過這兩層封裝在使用層面上將 window 降級 view,使用 Flutter 就可以和使用 Control 或者 Widget 一樣方便。FlutterProcessManager 負責管理分進程,當創建 FlutterContainerView 時,如果分進程還沒啟動,則喚起分進程 IPCController 則負責和 Flutter 進行通信,通過 FlutterContainerView 告知分進程打開指定的 Flutter 頁面。
封裝之後,視窗的層次關係如下。Flutter 只負責展示業務內容,視窗的屬性、樣式等,都通過企業微信來設置。通過和其他 View 進行組合使用,可以達到如圖所示的效果。
3: windows 文字渲染以及陰影等問題
win 在文字渲染上遇到兩個比較嚴重的問題:
文字渲染的細節不對
這裡是因為 Flutter 預設使用 skia 的渲染模式是 grayscale 灰度字體渲染方式,但是在 win 客戶端普遍使用的是 subpixel 渲染方式。導致文字渲染跟 win 有一些區別,這部分需要我們通過修改 engine 來修複核心代碼為:
SkPixelGeometry pixel_geometry = kUnknown_SkPixelGeometry;
#ifdef WIN32
UINT structure = 0;
if (SystemParametersInfo(SPI_GETFONTSMOOTHINGORIENTATION, 0, &structure, 0)) {
if (structure == FE_FONTSMOOTHINGORIENTATIONRGB)
pixel_geometry = kRGB_H_SkPixelGeometry;
else if (structure == FE_FONTSMOOTHINGORIENTATIONBGR)
pixel_geometry = kBGR_H_SkPixelGeometry;
}
#endif
SkSurfaceProps surface_props(0, pixel_geometry);
#ifdef WIN32
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
修複前:
修複後:
渲染字體錯亂
在某些 win 的機型上,如果當前系統語言不是簡體中文,Flutter 渲染的字體會有明顯的誤差,文字展示比較奇怪,不是標準的簡體中文。
主要原因是,Flutter 在渲染字體的時候,用系統當前預設的字體去渲染,當前的字體如果無法渲染這個文字,就會自動匹配一個字體來完成這個文字的渲染,這裡由於 skia 的匹配演算法匹配到了其他語言去,因此導致了渲染文字出錯。
Flutter text 組件中提供了一個文字渲染失敗的回調 fontFamilyFallback ,如果當前字體無法渲染字元的時候,會回調到 Flutter 上層,可以由 Flutter 上層指定要用字體,這裡我們給這個回調指定了微軟雅黑,從而解決語言錯亂的問題。
修複前:
修複後:
4: 應用獨立部署調試
整個環境搭建起來之後,因為 Flutter 四端跨平臺的能力,移動端的同學也能夠去開發一些桌面端的應用,但由於是混合開發的模式,開發別的平臺應用的時候,需要別對應平臺的工程代碼,並且不同平臺的開發環境以及倉庫都不一樣,桌面端工程的開發對於移動端的同學來說非常不方便。
現有的組件化模式本質還是一個大倉全代碼的編譯過程,雖然代碼按模塊隔離了,但是編譯的時候沒有做到隔離,debug 階段還要嚴重依賴宿主工程。
為了提高開發以及走查的效率,我們將 Flutter 的主工程拆分為多個微應用,為每個業務模塊提供 example application 的運行的能力,並且在 example 中依賴於 runner 的基礎組件,runner 主要提供 grpc 的遠程調用服務,負責將 channel/dart2cpp 的介面通過 grpc 遠程調用發送給服務端,這裡的服務端就是我們的宿主 app,通過這種模式,在調試階段,將 Flutter 應用完全從企業微信的宿主 app 裡面解耦開來,帶來的好處是,更快的編譯速度,更全的平臺開發體驗,更穩定的調試系統。
最後,在開發 Flutter 業務的時候,我們只需要 debug 版本的企業微信應用程式即可與原生進行通信,業務模塊只需要依賴 Flutter 環境就可以獨立運行起來。
七、總結
企業微信使用 Flutter 統一了四端的 UI 開發框架,在業務開發上效率得到了明顯的提升,以企業微信首個跨四端的大型應用人事助手為例,相比於四端獨立開發,使用 Flutter 作為跨平臺開發,整個需求的迭代協同效率大大提升:
1: UI 開發上我們統一了 dart 技術棧,不同平臺的同事都可以參與到 Flutter 開發當中來,解決桌面端人力不足的問題。
2: 通過移動端跨平臺+桌面端跨平臺的方案+ mvvm 的架構,我們研發效能提升 1 倍以上。
3: 對於設計/產品走查都只需要移動端和桌面端各走查一次,測試對 ui 渲染層也只需要測單端,節約了 1 倍的人力。
得益於移動端的模塊化架構,桌面端的工程可以很好復用移動端已有的基礎組件能力。我們將 ui 的數據以及交互從各端 UI 中分離,由 provider 進行統一的處理,來簡化各端 UI 上的開發成本,桌面端和移動端 UI 開發只需要簡單的佈局即可,結構如下:
例如在人事助手的首頁中待處理消息的列表卡片 UI,兩個卡片無論佈局還是顯示效果都有明顯的差別,在 UI 上不能完全復用。
桌面端
移動端
通過上述的開髮結構,整體的流程如下:
1: 通過 mixin TodoInfoAdapter 的方式,約束各平臺 UI 組件所需要的數據欄位,以及交互。
2: 桌面端和移動端分別使用對應的 ui 進行佈局,將 ui Widget 和 TodoInfoAdapter 進行數據的綁定。
3: provider 作為 viewmodel, 在初始化的時候通過 cgi 請求,對 proto 數據進行處理,這裡與 model 層進行交互。
4: provider 將 cgi 的 resp 中的相應數據轉換成為 ToDoInfoAdapter,轉換成功之後通過 notifyListener 刷新 ui 的數據。
5: provider 根據 resp 中的部門 id, 非同步拉取部門的數據,拉到數據之後,更新 adapter,調用 notifyListener 重新更新 ui 數據。
對於 cell 的點擊事件,也是作為 adpater 中的一個參數,在 viewmodel(HrSystemHomeProvider) 中統一處理。
由於四端的代碼復用,桌面端首頁卡片 Cell 減少了大約 48%的重覆代碼。
目前企業微信也在不斷利用和完善 Flutter 四端的能力,也在自研引擎上修複了不少 Flutter 的問題,提高 Flutter 在跨平臺上的開發體驗。
作者:yamichonghe
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Cross-terminal-integration-practice-of-WeChat-Flutter-and-large-native-projects.html