
跨平臺開發框架是客戶端領域的經典課題,幾乎從作業系統誕生開始就是我們軟體從業者們的思考命題,為了促進 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/545154.html
標籤:其他
