一. 背景 企業(yè)微信的跨平臺(tái)之路 二. 企業(yè)微信Flutter工程架構(gòu) flutter 多模塊架構(gòu) flutter為我們提供了四種不同的工程模塊
最終我們的業(yè)務(wù)代碼通過package 純dart來實(shí)現(xiàn),通過channel 生成的雙端代碼,由native各自的模塊維護(hù),如果是第三方的sdk或者插件則由plugin的方式引入,減少aar或frameowk的產(chǎn)生。 另外在基礎(chǔ)庫上我們下層了一些ui控件庫,基礎(chǔ)工具,和路由相關(guān)的組件。通過channel pigeon 的方式實(shí)現(xiàn)了我們的線上crash監(jiān)控,我們最終的組件化架構(gòu)可以設(shè)計(jì)為: 對(duì)于pacage 組件中各個(gè)模塊之間的相互調(diào)用,可以設(shè)計(jì)dart api文件對(duì)應(yīng)要暴露出去的接口,文件主要在存放在lib 目錄下,組件提供一個(gè)統(tǒng)一個(gè)對(duì)外暴露的Dart文件,內(nèi)部的細(xì)粒度的Dart實(shí)現(xiàn)通過export導(dǎo)入,這種設(shè)計(jì)思想正是Flutter官方Api的設(shè)計(jì)。
三. 混合棧開發(fā) 什么是混合棧?簡單來說,就是app中同時(shí)存在原生和flutter頁面,并且互相跳轉(zhuǎn)。 除了部分新的app,現(xiàn)在市面上大多數(shù)app引入flutter,都是以混合棧的形式引入。
對(duì)比FlutterBoost、FlutterThrio的混合棧方案,F(xiàn)lutterBoost入侵了原生Flutter navigator棧,將棧統(tǒng)一由原生或者flutter內(nèi)部管理的方式,而FlutterThrio則是直接使用flutter導(dǎo)航棧。 相比之下: FlutterBoost在企業(yè)微信的接入flutter 初期,一直停留在flutter低版本,并且對(duì)于flutter 的sdk 有一定的入侵性,經(jīng)過考量,企業(yè)微信實(shí)現(xiàn)一套flutter內(nèi)部導(dǎo)航棧的方案,并且遵循官方的路由設(shè)計(jì),設(shè)計(jì)AppContainer作為基礎(chǔ)容器,在engine初始化的時(shí)候,先預(yù)熱這個(gè)AppContainer容器,并進(jìn)行基礎(chǔ)配置、主題設(shè)置等操作,具體頁面打開時(shí)候,通過channel 來push 一個(gè)MaterialApp 的OverlayEntry 來做具體的路由棧,flutter內(nèi)部的跳轉(zhuǎn)由flutter 內(nèi)部實(shí)現(xiàn)。 這樣做的好處是收攏了基礎(chǔ)邏輯,業(yè)務(wù)開發(fā)時(shí)只需要關(guān)注業(yè)務(wù)邏輯,并且方便進(jìn)行全局層面的配置,提供了統(tǒng)一的導(dǎo)航棧插樁能力,對(duì)于flutter 的導(dǎo)航也沒有入侵性,都是通過Navigator 來控制路由。 除了棧管理之外,混合棧還有一個(gè)需要關(guān)注的問題,是engine的使用管理。 混合棧的頁面棧形式,棧中往往會(huì)出現(xiàn)多個(gè)flutter頁面,flutter的頁面和engine之間存在綁定關(guān)系,而flutter engine開銷很大,為每個(gè)flutter頁面綁定一個(gè)engine,不現(xiàn)實(shí)。 針對(duì)引擎的使用方案,企業(yè)微信從引入flutter至今,可以大致分為兩個(gè)階段: 1. 單引擎階段: 在flutter 2.0以前,我們使用單引擎模式,engine初始化后將被緩存下來,每個(gè)flutter頁面打開時(shí),都和這個(gè)engine綁定,這樣app中就只會(huì)有一個(gè)engine的開銷。 然而,混合棧的頁面棧形式,往往會(huì)出現(xiàn) 原生頁面->flutter頁面->flutter頁面 ,在flutter1.20版本的的前期,我們的這種路由設(shè)計(jì)無法支撐而多個(gè)flutter頁面共存于棧中,所以我們限制了flutter->flutter的場景不允許進(jìn)行容器之間跳轉(zhuǎn), 但是后面有一些浮窗的業(yè)務(wù)場景讓我們不得不打破這個(gè)限制,為了解決這種業(yè)務(wù)場景我們使用了獨(dú)立的flutter engine。 2. 多引擎階段: 解決這個(gè)問題最好的方式,就是支持多引擎模式,并解決由此帶來的內(nèi)存開銷問題。此時(shí)業(yè)內(nèi)的解決方案,多是修改engine源碼,復(fù)用多個(gè)engine的內(nèi)存空間。但這樣帶來的問題也很多,修改的engine方式始終落后官方engine版本,適配成本高,且往往會(huì)出現(xiàn)很多難以預(yù)測的問題。 恰逢此時(shí),flutter發(fā)布了2.0版本,官方提供了FlutterEngineGroup,以支持engine內(nèi)存空間的復(fù)用,徹底解決了多engine的內(nèi)存開銷問題。我們對(duì)多引擎的效果進(jìn)行了分析,新增一個(gè)engine的內(nèi)存開銷大概在4MB左右。 不過,雖然內(nèi)存開銷問題得到了解決,但engine初始化的耗時(shí),仍是一個(gè)不可忽視的問題,為了優(yōu)化體驗(yàn),我們并沒有直接使用多引擎與flutter頁面進(jìn)行1對(duì)1的綁定。而是采用了主引擎+臨時(shí)引擎的多引擎復(fù)用模式。 對(duì)于flutter頁面打開時(shí),棧中不會(huì)存在其他flutter頁面的情況,使用主引擎; 對(duì)于flutter頁面打開時(shí),棧中可能存在其他flutter頁面的情況,使用臨時(shí)引擎,同時(shí),頁面自定義一個(gè)引擎名稱,臨時(shí)引擎初始化后也將被緩存,這個(gè)頁面再次打開時(shí)將繼續(xù)使用這個(gè)臨時(shí)引擎,以優(yōu)化頁面啟動(dòng)速度。 整體來看: 原生側(cè),我們通過引擎復(fù)用來減少性能的消耗,通過引擎預(yù)加載來減少首次啟動(dòng)的時(shí)間。 在flutter側(cè),通過一個(gè)統(tǒng)一的AppContainer容器來作為頁面載體,在引擎初始化的時(shí)候,即預(yù)熱該容器。在實(shí)際頁面打開的時(shí)候,根據(jù)不同的路由,使用AppContainer來切換不同的子頁面。這樣相比于官方每次打開flutter頁面,都進(jìn)入一個(gè)新的頁面的做法,統(tǒng)一了flutter頁面入口,減少了大量原生與flutter交互的成本。 四. Flutter通信建設(shè) flutter與native的通信 1. 為什么需要pigeon 在flutter開發(fā)中,我們需要通過channel 的方式與native進(jìn)行通信,在多端的實(shí)踐過程中,我們發(fā)現(xiàn)channel存在一些問題: 因此官方推薦使用pigeon來維護(hù)我們的channel代碼,pigeon 將 我們定義的接口,通過dart的反射將class轉(zhuǎn)換成map的數(shù)據(jù)結(jié)構(gòu),并生成各端接口。簡化了我們平時(shí)手寫channel 和對(duì)接協(xié)議所帶來的成本。 2. pigeon的問題 企業(yè)微信是億萬級(jí)的項(xiàng)目,業(yè)務(wù)場景也十分復(fù)雜,在實(shí)際接入使用pigeon 的過程中,受到了非常大的業(yè)務(wù)挑戰(zhàn),在使用中發(fā)現(xiàn)pigeon還是存在著不少的問題。 比如:數(shù)據(jù)類型的支持較弱,不支持list和map 為什么不支持List和map呢?其實(shí)跟pigeon 傳輸?shù)臄?shù)據(jù)結(jié)構(gòu)有關(guān)。 channel 支持基礎(chǔ)的數(shù)據(jù)類型,其中就包含了map,pigeon在解析dart class的時(shí)候?qū)嶋H是將class轉(zhuǎn)換成map,再傳輸給native,native再以map的結(jié)構(gòu)反解成class,在正常的數(shù)據(jù)下似乎是沒什么問題,但是遇到List和map,由于沒有json那樣的反序列化工具,toMap和fromMap 的代碼的復(fù)雜度就會(huì)急劇上升,我們?cè)?jīng)為了支持list的結(jié)構(gòu),改造pigeon的部分源碼,直接映射List 的數(shù)據(jù)結(jié)構(gòu),對(duì)于一些基礎(chǔ)類型,并沒有什么很大的改造成本,但是遇到object 就需要繼續(xù)對(duì)object 進(jìn)行toMap 的操作: 如果List和map相互嵌套,對(duì)框架的生成來說代碼邏輯十分復(fù)雜,而且生成的代碼也會(huì)特別臃腫。 3. pigeon 的傳輸數(shù)據(jù)結(jié)構(gòu)優(yōu)化 List在我們實(shí)際的開發(fā)中使用的地方非常多,因此我們對(duì)pigeon 源碼進(jìn)行了改動(dòng)目的是為了: 由于protobuf在企業(yè)微信有大量地在使用,因此我們考慮能否將pigeon 的data class轉(zhuǎn)換成proto的數(shù)據(jù)結(jié)構(gòu),不僅能夠解決List/Map等數(shù)據(jù)的問題,對(duì)與已有的一些pb結(jié)構(gòu)也能起到很好的復(fù)用作用,因此我們沿著這個(gè)思路,優(yōu)化了pigeon 在生成代碼上的思路,具體流程如下: 在pigeon 生成class 的階段,我們hook 生成map的過程,改為生成proto,再編譯proto到各自的平臺(tái)上,由于proto 支持list和map,而且序列化和反序列化都有現(xiàn)成的工具,對(duì)于現(xiàn)有的工具鏈來說幾乎是零成本,而且我們還能復(fù)用已有的proto,避免了重復(fù)的數(shù)據(jù)轉(zhuǎn)換。 4. pigeon channel 注冊(cè) pigeon生成的server,需要在activity中注冊(cè)后,flutter頁面才能通過channel調(diào)用native的實(shí)現(xiàn)。然而,業(yè)務(wù)產(chǎn)品功能的變更,往往會(huì)讓兩個(gè)一開始設(shè)計(jì)的兩個(gè)獨(dú)立頁面,需要相互跳轉(zhuǎn)。flutter頁面的跳轉(zhuǎn),在dart側(cè)通過flutter的navigator即可完成跳轉(zhuǎn),此時(shí)承載flutter頁面的activity容器還是原來的界面,這個(gè)activity容器并沒有注冊(cè)新的flutter頁面的channel server。如:
Activity A 包含 Flutter頁面A Activity B 包含 Flutter頁面B 此時(shí)打開Activity A,將注冊(cè)Flutter頁面A的channel server。 再從Flutter頁面A跳轉(zhuǎn)至Flutter頁面B,此時(shí)activity棧仍在Activity A中。 Flutter頁面B的channel server沒有得到注冊(cè),如果此時(shí)調(diào)用Flutter頁面B的channel,將因?yàn)檎也坏綄?shí)現(xiàn)類而拋異常。 設(shè)計(jì)方案: 問題的難點(diǎn),在于Anroid的channel server實(shí)現(xiàn)類,分散在不同的module中,跨module手動(dòng)注冊(cè)其他flutter頁面的channel server實(shí)現(xiàn)類,繁瑣且不夠優(yōu)雅,而且不同的flutter頁面,往往是由不同的開發(fā)同事完成,互相的調(diào)用往往并不清楚哪些需要注冊(cè)channel server,一旦遺漏,就會(huì)產(chǎn)生異常,且這種異常,由于業(yè)務(wù)路徑的特殊性,開發(fā)和測試都難以檢測出來,風(fēng)險(xiǎn)性更大。 因此,設(shè)計(jì)了channel server的自動(dòng)化注冊(cè)流程: 整體流程原理如下: 五. dart與c++ 調(diào)用演進(jìn) 1. 企業(yè)微信客戶端的架構(gòu) 企業(yè)微信底層chroumin service 業(yè)務(wù)層級(jí)的跨平臺(tái)開發(fā)模式架構(gòu)已經(jīng)非常成熟和穩(wěn)定,而且擁有比較完善的工具鏈,如圖所示,Android和IOS主要負(fù)責(zé)UI繪制與聯(lián)調(diào),將與網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)處理等復(fù)雜的邏輯都交給c++層來處理。
在接入flutter 之后,重新在flutter上實(shí)現(xiàn)一套service和network無疑是巨大的成本,我們的首要目標(biāo)就是要復(fù)用底層跨平臺(tái)的邏輯,為了復(fù)用我們已有的工具鏈, 不可避免地需要解決dart與c++的相互調(diào)用問題。 2. flutter調(diào)用cpp dartvm 提供了Dart_SetNativeResolver 的方法來加載dart上標(biāo)記了native的方法, dart 與engine的通信方式也是基于這種方式來進(jìn)行的,在flutter engine 中我們能找到大量的RegisterNatives 方法,其中參數(shù) DartLibraryNatives 里面就存儲(chǔ)著我們的方法簽名,最后再通過Dart_SetNativeResolver 來加載dart 上標(biāo)記了native的方法。 Dart_Handle result_code = Dart_SetNativeResolver(parent_library, ResolveName, NULL); 因此我們可以通過修改engine 的方式,將flutter engine 內(nèi)部 RegisterNatives 以及Dart_SetNativeResolver 方法暴露出來并在合適的時(shí)機(jī)裝載自己的c++ 模塊,但是這種模式需要維護(hù)engine,而且對(duì)我們后面的升級(jí)和維護(hù)帶來很大的不便。 3. dart::ffi 調(diào)用 dart 在2.5 之后實(shí)現(xiàn)了dart::ffi 來調(diào)用c++的接口,并且在flutter上也得到了支持,但是dart::ffi在實(shí)踐的過程中依然有一些限制條件: 第一個(gè)問題,看下如果dart調(diào)用c++的同步接口,首先要在dart上綁定c++的方法,綁定過程包括范形和參數(shù)這些。 final loggerFunction = _dl.lookupFunction< Void Function(Pointer<Uint8>, Int32,Int64), void Function(Pointer<Uint8>, int,int)>("Logger"); c++的對(duì)應(yīng)實(shí)現(xiàn)如下 WE_DART_EXPORT void Logger(uint8_t * string, int32_t type,int64_t length) 可以看到其中理解需要一定的成本,而且在編寫代碼的過程一定要對(duì)齊參數(shù)。 第二個(gè)問題,如果c++的方法是一個(gè)異步接口,c++回調(diào)dart,異步回調(diào)的核心思路是在dart isolate 啟動(dòng)一個(gè)listenPort的監(jiān)聽函數(shù),在c++中,我們可以通過Dart_PostCObject 的方法來將某個(gè)function 的指針傳給dart,dart再通過ffi在flutter的ui線程上執(zhí)行這個(gè)function,其中的關(guān)系和邏輯相對(duì)復(fù)雜。 第三,如果dart與c++相互調(diào)用傳遞的數(shù)據(jù)是bytes,string這種,都是通過指針來傳遞,dart上提供了Pointer類,和malloc/free函數(shù),如果bytes的數(shù)據(jù)要傳遞到c++,則需要先在dart上分配堆上的uint8指針內(nèi)存,數(shù)據(jù)回調(diào)回來也類似,先將c++的pb數(shù)據(jù)轉(zhuǎn)換為 uint8 指針之后再回調(diào)給dart,內(nèi)存在c++分配之后,回調(diào)給dart,c++底層接口無法知道dart 上數(shù)據(jù)內(nèi)存什么時(shí)候用完,只能交給dart來處理,而且dart的開發(fā)者和c++的函數(shù)都要時(shí)刻保持著指針操作的風(fēng)險(xiǎn)。 4. ffi::gen ffi::gen是官方后來推出的自動(dòng)生成ffi接口的工具,ffi::gen我們依然沒有采用的主要原因是,沒辦法解決c++層代碼維護(hù)困難,膠水代碼,以及線程安全等問題。 5. ffi接口自動(dòng)生成與管理 企業(yè)微信在2020年下開始使用flutter作為大型獨(dú)立應(yīng)用開發(fā),通過dart::ffi 的方式復(fù)用了原有底層的service 架構(gòu),在一定程度上提高了開發(fā)效率,但是在實(shí)際開發(fā)過程中,每一次的業(yè)務(wù)需求都伴隨著大量dart::ffi 的膠水代碼,并且dart::ffi的方式類似于jni 的開發(fā)方式,一方面需要在dart/c++ 寫一套中轉(zhuǎn)的膠水代碼,另一方面由于dart::ffi 的調(diào)用 方式需要進(jìn)行線程的切換,并且dart 提供了指針的分配與釋放,內(nèi)存的管理似乎變得不太安全。 綜合以上我們希望對(duì)dart調(diào)用c++,做一些業(yè)務(wù)調(diào)用上的改進(jìn),主要目的是為了: 為了解決以上這些問題,我們希望能夠更加方便地調(diào)用c++的方法,因此參考grpc/trpc 實(shí)現(xiàn)了一套dart::ffi的簡單的rpc。在引入這套rpc工具后,對(duì)開發(fā)效率有顯著的提升。在proto上定義dart調(diào)用c++的接口,數(shù)據(jù)結(jié)構(gòu)統(tǒng)一為proto,c++層引入rpc的部分能力,dart層也引入相應(yīng)的stub,我們?nèi)サ魊pc的通信機(jī)制,改為dart::ffi來進(jìn)行client和server的通信,只在c++層引入至關(guān)重要的服務(wù)發(fā)現(xiàn)與服務(wù)調(diào)用。整體的架構(gòu)如下: 接下來我們需要調(diào)用c++的方法的過程為: final GovernRpcServiceApi api = GovernRpcServiceApi(WeRpcClient());final RpcResult<GetGovernMyReportListResp> result = await api.getGovernMyReportListFromServer(GetGovernMyReportListReq()..limit = 10); dart調(diào)用c++的方法,就跟調(diào)用本地的異步方法一樣。 調(diào)用過程如下 : 從整體的流程看,除了虛函數(shù)的實(shí)現(xiàn)需要業(yè)務(wù)邏輯方自己處理之外,其他的能力幾乎是全自動(dòng)生成,后臺(tái)和客戶端也可以共用一份rpc的proto。 六.flutter性能優(yōu)化 1. flutter著色器卡頓 flutter著色器卡頓問題 在實(shí)際的flutter 體驗(yàn)中,我們注意到一些首次進(jìn)入復(fù)雜的頁面會(huì)存在卡頓以及首次進(jìn)入flutter白屏的問題。根據(jù)官方的資料 https://v/docs/perf/rendering/shader 通過trace-skia 跟蹤了主要的耗時(shí)點(diǎn): 在啟動(dòng)的過程中我們發(fā)現(xiàn)skia的GPURasterizer::Draw 有持續(xù)的耗時(shí),有些耗時(shí)甚至達(dá)到了 597.016 ms,存在嚴(yán)重的卡頓問題。 這屬于skia 著色器卡頓的一部分,但是在2.3 之前,ios skia 的持久緩存會(huì)失效,直到2.3 beta之后skia 支持了ios metal 渲染。 針對(duì)add2app的方式優(yōu)化 但是著色器的卡頓處于初級(jí)階段,針對(duì)于add2app的方式,很多命令行都不適用,我們跟蹤了flutter的編譯源碼,最終發(fā)現(xiàn)在ios上可以通過 launchArguments添加一些flutter 的啟動(dòng)變量,例如 flutter run --profile --cache-sksl --purge-persistent-cache 在add2app 的方式下在實(shí)現(xiàn)為: 生成相應(yīng)的著色器之后,我們只需要將io.flutter.shaders.json 放在項(xiàng)目的根目錄,并且加到asset 中 flutter: assets: - io.flutter.shaders.json 2. 圖片緩存框架建設(shè) flutter本身沒有磁盤緩存能力,pub社區(qū)提供了很多解決方案,一般主流的 cached_image_network 緩存使用了 flutter_cache_manager 庫來實(shí)現(xiàn) cached_image_network雖然提供了硬盤緩存能力,但flutter在項(xiàng)目中以混合棧形式集成,原生本身也已經(jīng)有緩存框架。如果使用cached_image_network,原生與flutter加載同一張圖片,仍然需要加載并存儲(chǔ)兩次,且原生的圖片下載,還有復(fù)雜的下載策略,cached_image_network框架無法支持定制化。 因此,我們自己實(shí)現(xiàn)了一套緩存框架,打通了flutter與native的圖片緩存,流程如下: 在無內(nèi)存緩存的情況下,通過channel通道,調(diào)起原生側(cè)的圖片緩存邏輯,加載硬盤緩存,如果硬盤緩存也沒有,再通過原生的網(wǎng)絡(luò)通道去加載圖片緩存 3. svg與iconFont轉(zhuǎn)換 flutter目前還沒有直接使用native圖片資源的辦法,所以大部分情況我們需要維護(hù)一套新的圖標(biāo)庫,但是經(jīng)過實(shí)踐發(fā)現(xiàn),flutter在渲染圖片的時(shí)候并不是特別完美:如果是在底部tab,點(diǎn)擊之后切換圖片這種情況,低端機(jī)型上,第一次點(diǎn)擊切換圖片的時(shí)候會(huì)稍微閃一下,而且png占的資源比較大,flutter上我們希望找一套穩(wěn)定好用的矢量圖標(biāo)。 svg不被官方所支持,依賴第三方的package, 在flutter里面運(yùn)用最多的就是字體圖標(biāo)(Icons),字體圖標(biāo)具備矢量,顏色可修改,并且渲染性能好等特點(diǎn),被flutter官方運(yùn)用于自身的MaterialIcons和CuptinoIcons中,我們因此也想實(shí)現(xiàn)一套屬于自己的Icon圖標(biāo)庫。
具體的資源構(gòu)建主要是針對(duì)svg來的,我們?cè)谒{(lán)盾上部署nodejs環(huán)境以及安裝gulp,藍(lán)盾通過監(jiān)聽項(xiàng)目svg資源的變化自動(dòng)生成IconFont.dart的索引、ttf文件、以及相應(yīng)的靜態(tài)html。 在使用Iconfont圖標(biāo)之后,我們的圖片體積有所下降,只剩下多色圖的png資源,并且開發(fā)中通過字體圖標(biāo)定制顏色和大小都非常方便。 七:flutter 生態(tài)建設(shè) 1. 多語言框架建設(shè) flutter本身沒有多語言框架支持,普通的做法是通過flutter_intl框架來管理多語言資源,但仍需要手動(dòng)篩選需要翻譯的資源,待翻譯后再手動(dòng)填入項(xiàng)目。
為了讓多語言框架實(shí)現(xiàn)閉環(huán),最大程度地減少開發(fā)階段的工作,我們需要用腳本建設(shè)來補(bǔ)足框架缺失的能力。 針對(duì)英文、繁體翻譯,我們需要開發(fā)兩套插件。其中英文翻譯需要人工翻譯,繁體翻譯可以依賴api自動(dòng)翻譯。 同時(shí),為了更好地提高開發(fā)階段的代碼書寫效率,我們也期望允許開發(fā)階段將文本hardcode寫到代碼中,并通過腳本工具來自動(dòng)提取hardcode的文本資源。 總結(jié)來說,我們需要建設(shè)的腳本如下:
string_extractor 文本提取工具 通常來說,開發(fā)者在文字資源編寫的時(shí)候,為了節(jié)省開發(fā)時(shí)間,不中斷開發(fā)時(shí)的思路,往往會(huì)先將文字資源hardcode編寫到代碼中。待功能開發(fā)完之后,再將hardcode的文字資源統(tǒng)一提取到統(tǒng)一資源管理類中。這樣后期的提取工作費(fèi)時(shí)費(fèi)力,且容易遺漏。 框架提供了string_extractor自動(dòng)化hardcode文本資源提取的IDE工具,只需要安裝到IDE中,使用快捷鍵option+e即可自動(dòng)識(shí)別頁面中的hardcode文本,并提取到.arb文件中。 如圖為string_extractor插件界面,支持自定義索引id前綴: 增量翻譯腳本 1. rescan_flutter: 基于java實(shí)現(xiàn)的腳本工具,用來實(shí)現(xiàn)中譯英翻譯,主要提供了兩個(gè)命令:
流程如圖: 2. conversion2_flutter:基于python實(shí)現(xiàn)的腳本工具,用來實(shí)現(xiàn)中譯繁翻譯,運(yùn)行后,將直接基于開源api,將項(xiàng)目中.arb文件中的中文文字資源翻譯為繁體文字資源,并自動(dòng)寫入.arb文件中。 2. flutter仿原生動(dòng)畫與ui組件 跨平臺(tái)的首要命題:體驗(yàn) 能否達(dá)到原生的體驗(yàn),是跨平臺(tái)的首要目標(biāo),目前flutter的應(yīng)用還是有比較明顯的特點(diǎn),這幾個(gè)特點(diǎn)主要集中表現(xiàn)在: flutter體驗(yàn)上的一些優(yōu)化 在flutter上我們實(shí)現(xiàn)了一套自己的ui控件庫,實(shí)現(xiàn)了一些仿原生ui和動(dòng)畫: 3. 暗黑模式適配 企業(yè)微信Flutter暗黑模式的落地 系統(tǒng)主題Theme Flutter 應(yīng)用的統(tǒng)一入口是MaterialApp, MaterialApp 提供了theme和darktheme來適配淺色模式和黑暗模式,F(xiàn)lutter提供的組件,比如Appbar,Button,頁面的默認(rèn)文字大小,如果用戶在沒有指定參數(shù)的情況下,會(huì)默認(rèn)從系統(tǒng)的主題里面讀取,與native不同的是,native大部分組件都是自己自定義的,flutter控件是通過組裝模式來生成新的控件的,其實(shí)就是說我們的組件大部分不過就是在官方的組件上套了一層。但是官方的組件又只會(huì)默認(rèn)讀取自己系統(tǒng)的主題,因此,我們只能通過修改官方主題的形式來達(dá)到盡可能地簡化組件的參數(shù)和適配黑暗模式目的。 以后在使用官方組件/實(shí)現(xiàn)與官方類似的控件的時(shí)候,如果是通用組件,優(yōu)先考慮在主題上設(shè)置通用參數(shù),然后才是自定義參數(shù)設(shè)置。 //主題定義dividerTheme: const DividerThemeData( color: WWKLightColor.color_7, space: 0.33, thickness: 0.33,));dividerTheme: const DividerThemeData( color: WWKDarkColor.color_7, space: 0.33, thickness: 0.33,),//?錯(cuò)誤寫法// const Divider(height:0.33,color: Darkcolors.color_7,)//使用方法const Divider(); 自定義的顏色 CupertinoDynamicColor 提供了顏色的動(dòng)態(tài)切換,因此我們可以將我們的顏色定義成一個(gè)CupertinoDynamicColor,并且通過extension 的方式 添加在context里面。 Color get color73 => CupertinoDynamicColor.resolve(const CupertinoDynamicColor.withBrightness( color: Color(0xff32c757), darkColor: Color(0xff38c95c)), _context); 企業(yè)微信落地 八. 企業(yè)微信Flutter調(diào)試工具 UiInsight-Flutter 隨著企業(yè)微信在更多業(yè)務(wù)場景下使用Flutter技術(shù),擁有一款和原生的UiInsight相似的效率工具成了研發(fā)、測試、設(shè)計(jì)的迫切需求。 功能對(duì)比
FlutterInsight 分為三個(gè)功能塊,除內(nèi)部集成了效率工具和性能工具之外,也可根據(jù)各業(yè)務(wù)定制擴(kuò)展功能。 入口 接入FlutterInsight后,將在界面上懸浮展示fps和dart虛擬機(jī)的堆內(nèi)存大小,單擊后可展示更多信息,雙擊將彈出dialog,dialog中可開啟各工具。
效率工具 用于提升flutter開發(fā)效率、幫助還原設(shè)計(jì)稿 當(dāng)前頁面信息 可查看當(dāng)前頁面中Scaffold元素對(duì)應(yīng)的widget名和文件名及代碼行數(shù)。 由于所有頁面基本存在Scaffold作為一個(gè)頁面的主體,Scaffold元素的信息在大部分情況下也可反映當(dāng)前頁面的信息。以Scaffold的信息代表當(dāng)前頁面的信息,可避免對(duì)各業(yè)務(wù)頁面的侵入。 控件信息拾取 支持選中某widget獲取對(duì)應(yīng)widget的詳細(xì)信息,如類名、所在文件、所在行數(shù)、x/y定位信 位置拾取 拖拽選中環(huán)可得到選中環(huán)中心點(diǎn)的x/y位置信息。 控件間距離測量 這是一種全新的交互方式,主要用于測量控件A某邊和控件B某邊之間的距離。
如圖1,選中控件A的某條邊后長按,可彈出對(duì)話框,點(diǎn)擊確定后,將確定控件A的該邊作為開始邊,拖拽選中環(huán),可實(shí)時(shí)得到選中環(huán)對(duì)應(yīng)選中邊和開始邊的距離,若兩條邊的相互平行,可得到相對(duì)距離,若垂直,則得不到相應(yīng)距離。 圖片檢查 用于測量圖片源數(shù)據(jù)的寬高與控件本身的寬高,以確定是否加載了過大的圖片 顏色吸管 通過拖拽選中環(huán)選中屏幕內(nèi)某像素點(diǎn)并得到對(duì)應(yīng)的色值信息 性能工具 幫助發(fā)現(xiàn)flutter應(yīng)用的性能問題 fps樹狀圖展示 為方便更直觀地查看fps的變化,支持以樹狀圖的形式查看fps 開啟大圖檢測 對(duì)Image組件配置了frameBuilder后,可在打開界面時(shí)候查看該Image是否出現(xiàn)加載的圖片遠(yuǎn)大于Image組件本身大小的情況: Image.network( "https://img95.699pic.com/photo/40070/2524.jpg_wh860.jpg", frameBuilder: FlutterInsight.instance.checkImage, width: 100, height: 50,) 可在圖片寬高遠(yuǎn)大于控件寬高的Image組件中看到大圖警告的圖標(biāo): 內(nèi)存詳情及泄露 如圖,開啟1后,F(xiàn)lutterInsight將監(jiān)控頁面的Scaffold元素是否泄露,若發(fā)生泄露,將在左上角展示相關(guān)信息。 點(diǎn)擊查看泄露情況:
MethodChannel調(diào)用 如圖開啟methodchannel調(diào)用后,接下來發(fā)生的methodchannel調(diào)用均可查看:
頁面層級(jí)及加載耗時(shí) 在本工具的彈出框可開啟頁面層級(jí)及加載耗時(shí)監(jiān)聽,如1,開啟后,每進(jìn)入一個(gè)新頁面都將展示對(duì)應(yīng)頁面的加載耗時(shí)和widget數(shù)量深度信息。 基于aop的方法耗時(shí)排行 FlutterInsight 提供了特有的功能,統(tǒng)計(jì)flutter的方法耗時(shí): flutter在編譯時(shí),首先由frontend_server將dart代碼轉(zhuǎn)換為中間文件app.dill,然后在debug打包下,轉(zhuǎn)換為kernel_blob.bin,release打包下,轉(zhuǎn)換為so或framwork。 flutter的Aop就是對(duì)app.dill進(jìn)行修改實(shí)現(xiàn)的。AspectD是閑魚針對(duì)Flutter實(shí)現(xiàn)的AOP開源庫,可實(shí)現(xiàn)對(duì)項(xiàng)目中的方法進(jìn)行插樁。全方法的插樁是我們基于AspectD進(jìn)行修改實(shí)現(xiàn)的: 方案一:在aop_impl.dart中,通過添加Execute注解對(duì)所有方法進(jìn)行插樁。在對(duì)類似build這種覆寫方法插樁時(shí),拿不到該方法對(duì)應(yīng)的library,將產(chǎn)生nonenull報(bào)錯(cuò),如: https://github.com/XianyuTech/aspectd/issues/124。 方案二:在aop_impl.dart中,通過添加Call注解對(duì)所有方法進(jìn)行插樁。這個(gè)方案可以得到工程中的所有方法被調(diào)用時(shí)的耗時(shí),但由于沒有調(diào)用點(diǎn),故無法得到如xxWidget的build方法的耗時(shí),也無法滿足我們的需求。 最終方案: 1. 首先app.dill將讀取為Component變量。 2. 通過遍歷該component中的library、class、procedure,可得到工程中寫的aop_map_help.dart文件,并保存其markStart和markEnd函數(shù)為procedure。以便后續(xù)添加markStart調(diào)用和markEnd調(diào)用時(shí)使用。 3. 考慮到一個(gè)方法的開始和結(jié)束存在以下幾種情況:
@overrideWidget build(BuildContext context) { int aa = 0; if(aa == 1)return Text("test"); return Container(); //結(jié)束時(shí)}
void test1() { int add =0; if(add == 0)return;} 4. 我們可以通過RecursiveVisitor 提供的api訪問app.dill中l(wèi)ibrary、class、blockreturnElement,并實(shí)現(xiàn)上述的插樁行為。 5. 插樁后解開app.dill可以看到:
在test的函數(shù)開始、return處或結(jié)束處均插入了對(duì)應(yīng)統(tǒng)計(jì)代碼。 6. 考慮到一般來說,我們更關(guān)注未被async修飾函數(shù)的耗時(shí),可以在3、4操作前通過讀取 procedure.function.dartAsyncMarker.index 先判斷當(dāng)前function是否為async函數(shù),具體判斷方式參考官網(wǎng)AsyncMarker enum,若為async函數(shù),則不執(zhí)行插樁。 擴(kuò)展工具: FlutterInsight支持各業(yè)務(wù)方根據(jù)自己的業(yè)務(wù)/技術(shù)特點(diǎn)增加入口,支持跳轉(zhuǎn)、展示、開關(guān)三種類型,如企業(yè)微信是通過底層native來訪問網(wǎng)絡(luò)和數(shù)據(jù)庫服務(wù),故而專為企業(yè)微信擴(kuò)展了native調(diào)用(方法名及耗時(shí))頁面的跳轉(zhuǎn)入口。 FlutterInsight.instance.addDialogItem(UiItemWidget( title: "native調(diào)用", showMore: true, onMorePressed: (context) { // 跳轉(zhuǎn) }, )); FlutterInsight.instance.addDialogItem(UiItemWidget( title: "打開測試模式", checked: true, onCheckBoxPressed: (isChecked){ }, )); 九:企業(yè)微信Flutter動(dòng)態(tài)化探索 1. 基于 Flutter 的動(dòng)態(tài)化方案 根據(jù) DSL 的不同,基于 Flutter 的動(dòng)態(tài)化方案可以分為兩大類:面向前端的解決方案和面向終端的解決方案。面向前端的解決方案主要基于 JS 或 TS 語言進(jìn)行開發(fā),對(duì)于前端同學(xué)更加友好,面向終端的解決方案主要使用 Dart 語言進(jìn)行開發(fā),使用 Android Studio、VSCode 等 IDE 進(jìn)行開發(fā),對(duì)終端同學(xué)更加友好,對(duì)于前端同學(xué)來講,有一定的學(xué)習(xí)成本。 面向前端的解決方案代表框架有 LiteApp 和 Kraken,LiteApp 由微信自研出品,Kraken 是阿里前段時(shí)間開源的;面向終端的解決方案代表框架是美團(tuán)出品的 MTFlutter(Flap),由于 MTFlutter 還未開源,短期內(nèi)也用不上,這里就不做過多介紹了,感興趣的同學(xué)可以自行查找資料學(xué)習(xí)。 下圖從開發(fā)語言/框架、通信效率、渲染效率、等四個(gè)角度,對(duì) LiteApp 和 Kraken 進(jìn)行了調(diào)研和對(duì)比: 1. 在上層業(yè)務(wù)開發(fā)時(shí),LiteApp和Kraken都提供了兼容W3C規(guī)范的DOM API,并將其暴露給 JS Engine,LiteApp 目前支持 Vue.js 的開發(fā),而Kraken支持HTML/CSS/React/Vue進(jìn)行開發(fā)。 2. 在跨端通信方面,Kraken 對(duì)官方的 dart:ffi 進(jìn)行了一定的改造,支持了 dart 和 c 的雙向調(diào)用;而 LiteApp 是對(duì) Flutter Engine 進(jìn)行改造,增加了 dart2cpp 模塊,暴露出部分 C++ 接口,使得外部的動(dòng)態(tài)庫可以基于這些接口通過 DartVM 調(diào)用到 dart 的接口。在 Dart 的運(yùn)行環(huán)境中 C++ 和 Dart 之間就可以像調(diào)用自身的接口一樣調(diào)用彼此的接口。 3. 在渲染效率方面,Kraken 不依賴 Flutter Widget,而是直接依賴 Render Object,這樣具備更短的渲染管線;LiteApp 是將解析生成的 Virtual DOM Tree 映射為 Flutter Widget Tree。從技術(shù)原理的角度看,Kraken 比 LiteApp 具備更優(yōu)秀的渲染效率。 4. 在兼容 W3C 規(guī)范方面,Kraken 對(duì) CSS 的支持比較弱,用于開發(fā)線上需求還不夠;相比之下,LiteApp 在這方面做的更好,比如:對(duì) CSS 的支持更加全面,并且可以寫在單獨(dú)的 CSS 文件中,支持富文本,支持 Store 等。 2. 企業(yè)微信 Flutter 動(dòng)態(tài)化方案 - LiteApp 如下圖所示是企業(yè)微信 Flutter 整體架構(gòu)示意圖,可以分為兩部分,底層是宿主企業(yè)微信主工程,上層包括兩塊,分別是基于 Flutter 的動(dòng)態(tài)化框架 LiteApp 和 Flutter 的原生開發(fā)。上層部分是從左至右的執(zhí)行順序,總共可以分為三個(gè)階段: 1. 前端同學(xué)使用 Vue.js 進(jìn)行業(yè)務(wù)開發(fā)(生成的 zip 包可以下發(fā)到終端),經(jīng)常 JSEngine(封裝后的 JavaScriptCor 和 V8)解析運(yùn)行,在內(nèi)置的 JS 基礎(chǔ)庫的支撐下生成 Virtual Dom Tree 2. 在 LuggageView 層映射為 LuggageView 樹,并進(jìn)行 CSS 屬性解析和布局,最后通過 dart2Cpp 模塊將 L?uggageView 樹傳輸?shù)?Flutter 層 3. Flutter 層解析生成對(duì)應(yīng)的 Element Tree → Component Tree → Widget Tree,這樣便可以在終端通過 Flutter Engine 渲染了 在企業(yè)微信中,目前已有小黑板、家校應(yīng)用、學(xué)習(xí)園地、設(shè)備巡檢四個(gè)業(yè)務(wù)使用 LiteApp 開發(fā)并上線(3.1.12 版本),目前也還有一些問題(比如:運(yùn)行內(nèi)存較高)正在優(yōu)化解決,期望后續(xù)會(huì)開源出來方便更多的開發(fā)者和業(yè)務(wù)。 回顧&展望 企業(yè)微信在開始大規(guī)模地使用flutter作為跨平臺(tái)開發(fā)后,承受住了各種業(yè)務(wù)需求的考驗(yàn),而且flutter頁面的占比也逐漸提高,以下是各版本flutter 使用占比率: 流程與效率提升: 實(shí)際項(xiàng)目迭代過程中,得益于flutter跨平臺(tái)的能力,各角色協(xié)同效率明顯提升 1. 研發(fā)側(cè):基于flutter各平臺(tái)技術(shù)棧統(tǒng)一,需求開發(fā)人力投入減少50% 2. 設(shè)計(jì)側(cè):基于flutter ui的一致性,設(shè)計(jì)側(cè)可以把主要精力放到ios平臺(tái),ui走查效率提升40% 3. 測試側(cè):對(duì)于flutter內(nèi)部閉環(huán)頁面單平臺(tái)人力就可以做到跨平臺(tái)覆蓋 對(duì)外影響力: Google IO 大會(huì)介紹 企業(yè)微信客戶端團(tuán)隊(duì),包括 iOS、Andrroid、Windows、Mac、Web 五大平臺(tái)。我們重視跨平臺(tái)技術(shù)框架的研發(fā),各類原創(chuàng)技術(shù)專利,截止去年,僅數(shù)十人的技術(shù)團(tuán)隊(duì)在近3年內(nèi)提交技術(shù)專利百余項(xiàng)。團(tuán)隊(duì)招聘優(yōu)秀技術(shù)人才,崗位分布在成都、廣州、深圳。歡迎在官網(wǎng)投遞簡歷。 可在 hr.tencent.com 搜索企業(yè)微信相關(guān)崗位,或者掃碼聯(lián)系 HR |
|