Flutter應用Windows平臺接入實踐詳解
前言
Windows應用開發有著較為豐富和多樣的技術選型。C#/WPF 這種偏Native的閉源方案,目前開發人員相對比較小眾瞭。C++/QT 的跨平臺框架,C++對於GUI開發來說上手會更難。JavaScript/CEF/Electron 基於Chromium 的跨端框架,使用前端技術棧來構建桌面應用,性能會略低一些。總而言之各有所長,有一點可以確定的是,跨端能力成為瞭選型的重要考量。
Flutter從誕生之初起,其核心目標就是跨平臺,不僅僅支持Android和iOS的移動端設備,同時包括桌面端和Web端。隨著2022年2月Flutter 2.10的推出,也帶來瞭首個支持Windows平臺的穩定版本。基於Flutter的跨平臺特性,移動端或Web端的Flutter應用也能夠在Windows系統上運行,Windows應用開發者能夠享受到Flutter開發帶來的便利和生產力上的提升,同時移動端開發者也能夠快速上手Windows應用開發瞭。
Windows平臺接入
在進一步探索和預演之後,通過Flutter的能力,可以很方便地將移動端的業務模塊遷移至PC端,盡可能地實現一碼多端,降低業務維護成本,以此為出發點,進行瞭Windows平臺的接入。
閑魚App已經在Android和iOS平臺上有瞭多年的積累,並且采用瞭Native和Flutter混合的技術方案,Flutter和Native相輔相成,共同組成瞭App的完整生態。如果想要讓Flutter相關的模塊在Windows平臺上運行,那就需要讓Windows平臺補齊Android和iOS平臺提供給Flutter的能力。比如通過Platform Channel提供給Flutter側相關的Native能力,通過Platform View將Native視圖嵌入到Flutter頁面中,都需要在Windows平臺上進行重新開發。
Windows平臺通過Plugin或FFI的方式提供相關能力,需要使用C++編寫相關的平臺代碼。如果Plugin的代碼可以自閉環,即所有C++代碼都可以在Plugin內編寫完成,那這個Plugin可以單獨抽成一個Dart庫。但是如果Plugin的代碼需要復用其他Plugin或者主工程的C++代碼,粗暴一點就是拷貝代碼,或者通過CMakeLists來控制相互之間的依賴關系,通過find_package來完成頭文件和庫文件的鏈接。一旦依賴關系比較復雜,CMakeLists就會變得臃腫,依賴關系發生變化時,也會牽一發而動全身。隨著系統復雜度的提升,開發人員的增加,模塊之間相互耦合在一起,單一模塊的修改都會影響到所有模塊。
針對上述的問題,對於底層的模塊化設計,梳理瞭需要遵循的設計原則:
- 單一職責原則:一個模塊維護一個單一的主要功能,劃清模塊間的職責邊界;
- 開閉原則:模塊應該對擴展開放,對修改關閉。用抽象構建框架,用實現填充細節,通過擴展實體來實現變化,避免修改代碼來實現擴展。
- 迪米特法則:最少知道原則,對依賴的模塊知道的越少越好,模塊除瞭對外暴露的方法,其他實現細節都隱藏在內部。
- 接口隔離原則:隻依賴需要的接口,模塊之間提供最小的接口實現依賴關系。
- 依賴倒置原則:依賴抽象,不依賴具體細節,模塊之間需要依賴抽象的架構,而非具體的模塊細節。
首先基於上述的設計原則,制定瞭模塊化拆解的XModule方案,依據職責來劃分模塊,設計對外暴露的抽象接口,抽象接口保持最小化原則,完成接口實現,編譯出模塊的動態鏈接庫DLL,依賴到主工程並放置到特定目錄,運行時通過插件機制進行動態加載。
其次針對模塊化帶來的依賴管理復雜的問題,引入瞭vcpkg的依賴管理方案,通過清單模式便捷地管理各個模塊,可以自動引入間接依賴,並且版本沖突問題也不復存在瞭。
結合XModule和vcpkg之後,最終形成瞭下面的結構,後面將詳細展開。
模塊化拆解XModule
上述是一個登錄模塊的例子,Module 作為基類,定義瞭模塊的一些生命周期方法。LoginModule是對外公開的業務接口,裡面僅包含外部會用到的和登錄業務相關的方法。LoginModuleImplV1類是登錄邏輯的具體實現,不對外公開,裡面的私有成員變量和方法對外部是隱藏的,同時實現瞭Module和LoginModule的接口。Provider用於創建和管理Module實例。
這裡采用的思路是,底層模塊和模塊之間,上層和底層之間隻依賴接口頭文件,頭文件內包含有限的需要對外暴露的接口。通過XModule這個框架,將實現和接口進行分離。
為瞭將接口和實現分離,用到瞭 pimpl (Pointer to Implementation) 的理念,將對象的實現細節隱藏在指針背後。LoginModule接口負責定義對外公開的API,LoginModuleImplV1類負責定義LoginModule的具體實現,也就是調用的指針實際指向的對象。調用方隻能知道LoginModule中公開的API,而無法知道LoginModuleImplV1的實現細節,可以降低調用方的使用門檻,也可以降低錯誤使用的可能性。pimpl不僅解除瞭接口和實現之間的耦合關系,還可以降低文件間的編譯依賴關系,起到“編譯防火墻”的作用,可以提高一定的編譯效率。
// LoginModuleProvider 通過宏自動生成 X_MODULE_PROVIDER_DEFINE_SINGLE(LoginModule, MIN_VERSION, MAX_VERSION); // LoginModuleImplV1Provider 通過宏自動生成 X_MODULE_DEFINE_SECONDARY_PROVIDER(LoginModuleImplV1, LoginModule);
XModule的模版開發方式,會增加很多類文件,為瞭方便,通過宏來控制Provider類的自動生成。其中MIN_VERSION和MAX_VERSION是該Module接口能支持的最小和最大的版本范圍,可以限制後期dll插件化加載時,不加載在版本之外的dll,避免產生沖突和錯誤,目前Provider的GetVersion使用的是MAX_VERSION。
// 由 X_MODULE_DEFINE_SECONDARY_PROVIDER 宏自動生成 class DLLEXPORT LoginModuleImplV1Provider : public LoginModuleProvider { public: LoginModule* Create() const { LoginModuleImplV1* p = new LoginModuleImplV1(); ((Module*)p)->OnCreate(); return p; } };
LoginModuleImplV1Provider可以通過調用Create方法拿到對應的LoginModuleImplV1實例。
x_module::ModuleCenter* module_center = x_module::ModuleCenter::GetInstance(); module_center->AcceptProviderType<LoginModuleProvider>();
ModuleCenter是所有Module的管理類,先通過x_module::ModuleCenter::GetInstance()拿到ModuleCenter的實例,它是一個跨dll的單例。然後要用之前的LoginModuleProvider去註冊一個Module類型到ModuleCenter中。LoginModuleProvider中定義瞭支持的Module類型,以及最小版本和最大版本,如果後續掃描到的dll中提供的對應類型的Provider中GetVersion返回的值不在最大版本和最小版本之間,那麼就不會被允許加載進來。
module_center->AddProvider(new LoginModuleImplV1Provider());
通過這種方式,可以將LoginModuleImplV1Provider註冊到ModuleCenter中,然後創建並管理LoginModuleImplV1的實例。但是這樣就顯式地依賴瞭LoginModuleImplV1Provider,違反瞭前面說過的依賴倒置原則,對開閉原則也不友好,因為這樣就隻能通過修改代碼來實現擴展瞭。
#include <x_module/connector.h> #include "login_module/login_module_impl.h" X_MODULE_CONNECTOR bool XModuleConnect(x_module::Owner& owner) { owner.add(new LoginModuleImplV1Provider()); return true; }
為瞭在加載dll時,來註冊Provider,增加瞭一個connector.cc,添加一個XModuleConnect方法,讓dll被加載之後,能夠找到XModuleConnect這個符號方法,並進行調用,在XModuleConnect被調用的時候,會調用AddProvider將Provider進行註冊。
std::string path = GetProgramDir(); module_center->Install(path, "login_module");
由於目前login_module.dll是直接放在exe同目錄的,所以這裡直接獲取瞭一下exe絕對路徑,然後調用Install方法,將路徑和dll名login_module傳入進去,這樣就完成瞭註冊。
auto* p_login_module = module_center->ModuleFromProtocol<LoginModule, LoginModuleProvider>(); if (p_login_module == nullptr) { (*move_result)->Error("-100", "login module 為空"); return; } bool islogin = p_login_module->IsLogin();
在使用時,隻需要LoginModule和LoginModuleProvider這兩個抽象,就能獲取真實的LoginModuleImplV1這個實例,調用方僅需關心LoginModule所公開的API,完全屏蔽瞭對實現的依賴。後續底層擴展成瞭LoginModuleImplV2,隻要LoginModule的公開API不變,對上層是無感知的。這種方式完全遵循瞭前面提到的設計原則,對團隊內的多人維護以及後續的更新迭代都帶來瞭穩定的保障。
基於vcpkg的C++依賴管理
模塊拆分之後,帶來的副作用就是依賴管理會變得更加復雜,到C++這邊就是CMakeLists的膨脹。從移動端的角度來看這個問題,Android可以通過Gradle來管理依賴,依賴庫構建成aar之後上傳到Maven倉庫,implementation 'androidx.recyclerview:recyclerview:1.1.0'
像這樣通過包名、庫名和版本號來依賴具體的庫。iOS有CocoaPods,通過添加pod 'AFNetworking', '~> 2.6'
到Podfile來完成依賴的添加。前端也有NPM這樣的包管理器,所有依賴都在package.json這個文件中聲明和管理。Flutter側也可以通過pubspec來管理各個依賴庫。為瞭獲得一致的體驗,解決C++側依賴管理的痛點,我們引入瞭微軟官方推出的vcpkg,vcpkg的清單模式可以得到類似的體驗。
依賴庫配置
這裡以fish-ffi-module模塊為例子,文件結構如下,其中include文件裡面是對外公開的頭文件,src文件包含當前庫內部使用的代碼,cmake文件下的config.cmake.in模版文件用於生成xxx-config.cmake的文件,用於被find_package找到。
. ├── CMakeLists.txt ├── LICENSE ├── cmake │ └── config.cmake.in ├── include │ └── fish_ffi_module.h ├── src │ ├── connector.cc │ ├── fish_ffi_module_impl_v1.cc │ └── fish_ffi_module_impl_v1.h ├── vcpkg-configuration.json └── vcpkg.json
vcpkg-configuration.json配置瞭私有源,後面會講到。vcpkg.json文件,聲明瞭當前庫所依賴的其他庫,即vcpkg的依賴清單,其中"dependencies"字段聲明瞭所使用的依賴名稱。
{ "name": "fish-ffi-module", "version": "1.0.0", "description": "A fish-ffi module based on fish-ffi-sdk.", "homepage": "", "dependencies": [ "fish-ffi-sdk", "x-module", "flutter-sdk" ] }
CMake工程最重要的就是CMakeLists文件瞭,裡面配置瞭編譯相關的設置,添加瞭相關的註釋來幫助理解。
cmake_minimum_required(VERSION 3.15) # 倉庫版本常量,升級時修改 set(FISH_FFI_MODULE_VERSION "1.0.0") project(fish-ffi-module VERSION ${FISH_FFI_MODULE_VERSION} DESCRIPTION "A fish-ffi module based on fish-ffi-sdk." HOMEPAGE_URL "" LANGUAGES CXX) option(BUILD_SHARED_LIBS "Build using shared libraries" ON) # vcpkg清單中添加依賴之後,通過find_package就能找到 find_package(fish-ffi-sdk CONFIG REQUIRED) find_package(flutter-sdk CONFIG REQUIRED) find_package(x-module CONFIG REQUIRED) # configure_package_config_file 生成config要用到 include(CMakePackageConfigHelpers) # install 安裝要用到 include(GNUInstallDirs) # 當前庫的頭文件和源文件 aux_source_directory(include HEADER_LIST) aux_source_directory(src SRC_LIST) add_library(fish-ffi-module SHARED ${HEADER_LIST} ${SRC_LIST} ) # 設置別名 add_library(fish-ffi-module::fish-ffi-module ALIAS fish-ffi-module) # 設置動態庫導出宏,PRIVATE為編譯時,INTERFACE為運行時 if (BUILD_SHARED_LIBS AND WIN32) target_compile_definitions(fish-ffi-module PRIVATE "FISH_FFI_MODULE_EXPORT=__declspec(dllexport)" INTERFACE "FISH_FFI_MODULE_EXPORT=__declspec(dllimport)") endif () target_compile_features(fish-ffi-module PUBLIC cxx_std_17) # 添加頭文件 target_include_directories(fish-ffi-module PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> ) # 鏈接庫文件 target_link_libraries(fish-ffi-module PRIVATE fish-ffi-sdk::fish-ffi-sdk) target_link_libraries(fish-ffi-module PRIVATE flutter-sdk::flutter-sdk) target_link_libraries(fish-ffi-module PRIVATE x-module::x-module) # 基於config.cmake.in的模板生成xxx-config.cmake的文件 configure_package_config_file( cmake/config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module NO_SET_AND_CHECK_MACRO) # 生成xx-config-version.cmake文件 write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake VERSION ${FISH_FFI_MODULE_VERSION} COMPATIBILITY SameMajorVersion) # 將上面生成的兩個config文件,安裝到share/fish-ffi-module下 install( FILES ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module) # 安裝頭文件 install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # install target install(TARGETS fish-ffi-module EXPORT fish-ffi-module-targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) # 導出 install(EXPORT fish-ffi-module-targets NAMESPACE fish-ffi-module:: DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module)
這裡面最重要的一點是配置xx-config.cmake和xx-config-version.cmake的生成,vcpkg會在源碼首次拉下來的時候進行編譯,編譯完在相應庫的share目錄生成上述兩個文件,並且在CMake配置階段執行,這樣在使用find_package的時候就能獲取到這個庫以及對應版本號。總結一下就是,vcpkg幫助完成瞭代碼的下載、編譯和配置,然後就可以方便的鏈接三方庫瞭。
自定義私有源
私有源的自定義非常簡單,其實就是個Git倉庫,push到私有的git托管服務上即可。隻需要將依賴庫的最新commit信息記錄到這個倉庫裡面,通過模版化的配置就能完成依賴庫的發佈。
. ├── ports │ ├── fish-ffi-module │ │ ├── portfile.cmake │ │ └── vcpkg.json │ └── x-module │ ├── portfile.cmake │ └── vcpkg.json ├── versions │ ├── f- │ │ └── fish-ffi-module.json │ └── x- │ │ └── x-module.json │ └──baseline.json └── LICENSE
vcpkg裡面對依賴庫的定義叫port,這裡定義瞭兩個port,分別是fish-ffi-module和x-module。其中的文件說明如下:
- portfile.cmake中定義瞭這個庫的git地址、分支、commitId、編譯配置等信息
- vcpkg.json定義瞭這個port的依賴以及版本信息,如果有依賴,則會在編譯這個庫之前優先編譯依賴。
- versions下的文件按首字母分類,裡面定義瞭version和git-tree的對應關系。在port新增或更新之後,git-tree需要重新生成,通過
git rev-parse HEAD:ports/x-module
來生成git-tree,然後通過git commit --amend
追加提交到剛剛的commit中。
在需要使用私有源的CMake工程根目錄,添加vcpkg-configuration.json,裡面內容如下。default-registry為默認源,指向官方的地址即可。registries下添加自定義的私有源,再通過指定packages,表示裡面的庫需要在這個私有源查找。這樣就完成瞭私有源的配置。
{ "default-registry": { "kind": "git", "repository": "https://github.com/microsoft/vcpkg", "baseline": "f4b262b259145adb2ab0116a390b08642489d32b" }, "registries": [ { "kind": "git", "repository": "xxx.git", "baseline": "1ad54586a5a2fadb8c44d3f8f47754e849fc5a38", "packages": [ "x-module", "fish-ffi-sdk", "fish-ffi-module"] } ] }
在versions文件夾下還有一個baseline.json的文件,這個文件主要是設置基線用的,不像其他的依賴管理工具,vcpkg主要是通過這個基線來設置當前所使用的版本號的。
vcpkg可以勝任依賴管理的相關工作,綜上所述隻是一個簡單使用,相比其他平臺的依賴管理工具略顯繁瑣,除此之外還有很多其他能力,需要到vcpkg.io的官方文檔裡面探索瞭。
總結
Flutter應用接入Windows平臺,主要遇到的問題就是Windows側的一些能力的提供,需要對齊Android和iOS的已有能力。因為使用的是C++的開發語言,對於移動端開發者並不是那麼友好,學習曲線相對會比較抖。不過一旦平臺側的能力完善之後,又可以回歸到Flutter這個熟悉的領域瞭,享受Flutter開發帶來的便捷。此外Windows應用的開發不僅僅隻是屏幕加大版的移動端開發,還包括不同的輸入設備(鍵盤鼠標)、交互習慣、樣式風格、操作系統特性等,為瞭更好的平臺體驗,會帶來一定的適配成本,這一塊後續也將持續投入。
以上就是Flutter應用Windows平臺接入實踐詳解的詳細內容,更多關於Flutter接入Windows平臺的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- C語言中find_package()的搜索路徑的實現
- Flutter iOS開發OC混編Swift動態庫和靜態庫問題填坑
- ROS系統將python包編譯為可執行文件的簡單步驟
- 解決IDEA JDK9沒有module-info.java的問題
- Android集成Flutter