閒魚對Flutter-Native混合工程解耦的探索

閒魚Flutter現狀

閒魚是第一個使用Flutter混合開發的大型應用,但閒魚客戶端開發最深入體會的痛點就是編譯時長影響開發體驗。在Flutter+Native這種開發模式下,Native編譯速度慢,模塊開發無法突破。閒魚集成了集團眾多中間件,很多功能無法通過flutter直接調用,需使用各種channel到native去調用對應功能。總而言之,閒魚目前Flutter開發面臨如下幾個痛點:

  • Flutter側混合編譯速度慢,Android首次編譯10min+,iOS首次編譯20min+

  • 混合棧編程中歷史包袱導致IOS/Android雙端返回給Flutter側的數據可能存在不一致性

  • 集成模塊開發效率相比模塊開發較低,單模塊頁面測試性能數據無法展開;

方案一:模塊化開發

方案概述

此項目從立項至今已經很長一段時間,由於業務迭代快,native插件滿天飛情況下,想要做到工程模塊化拆分難度可想而知;如下圖是項目立項為模塊化拆分,業務方需要將各個業務拆分解耦合,拆分集團中間件,業務封裝組件,Native業務代碼,Flutter橋代碼,Flutter組件庫,Flutter側業務代碼等多個模塊;項目初衷就是整理代碼,提供一個Flutter可運行的乾淨環境,同時需要讓flutter可以獲取到native幾乎所有能力,但是編譯開發調試時候有想要速度快,效率高。能想到的最直接解決方案就是拆包,從0-1建立一個最小殼工程,然後拆分集團基本中間件,封裝業務組件,Flutter插件等,如下是整個項目架構:

閒魚對Flutter-Native混合工程解耦的探索

日常模塊化單頁面級需要使用最小殼工程,其內部有channel的聲明和實現,通過運行最小殼工程運行得到結果,Flutter側模塊開發通過IOC調用到最小殼工程的channel得到返回結果,最後將模塊化開發以一種pub或者git依賴方式集成到閒魚FWN主工程即可;

階段性效果

業務模塊化拆分從來都是一種吃力不討好的活,明知道拆出來有收益,但是投入產出比不足,因此歷史包袱代碼越來越厚重,以至於下一個接收的人都不敢輕易修改代碼;在模塊化拆分時候,開始項目時候提出過新起一個乾淨的工程,然後一步步拆分集團中間件,期間拆出了
Mtop/Login/FlutterBoost/UI Plugin,耗時3周/2人,得到部分結果就是新業務,新界面開發滿足基本快速迭代開發,缺點也很明顯如下所示:

  • 拆分梳理Native的中間件繁瑣,工作量巨大,最小化殼工程耗時3周/2人

  • 推動業務方拆分基礎組件庫更難,目前項目進展不順

  • 維護成本高,拆分殼工程運行結果和主工程可能不一致

  • 業務迫切其結果,但投入產出比不足,比如Flutter單頁面性能測試,Flutter側模塊化拆分,Fass工程一體基石

方案二:跨進程開發

換位思考

  • 若自己是業務方,需要為Flutter側去拆分包,去構建一個最小化殼工程,其成本是巨大的。

  • Fass工程一體化依賴一個最小化殼工程的Native運行環境去運行Flutter側代碼,可是並非所有的業務方都會提供一個最小化殼工程去運行Fass,那麼Fass工程一體化/模塊開發如果在集團其他運行環境下進展?

  • 最小化殼工程運行環境無法緊跟Native側的各種版本,會導致運行結果不一致情況下也不敢隨便使用;

如果解決此問題呢?個人提出過跨進程實現方式,在Android端側跨進程調用實現方式一直很常見的場景,client訪問server的結果,而Flutter側和Native側不就是client和server雙端麼?如下圖所示,其實Flutter獲取數據就是通過
MethodChannel/EventChannel獲取,因此可以換一種方式思考?

閒魚對Flutter-Native混合工程解耦的探索

IPC跨進程通信,Android Binder

期間在Android側我使用過Android Binder去實現,新起一個APP做為殼工程,其內部實現了各種插件去訪問主工程服務,獲取結果然後返回給殼工程的Flutter調用,但是維護成本依然在;同時iOS側沒有對應的實現機制,因此此方式被拋棄;

具體方案:Hook代理+Socket服務

Android開發應該都熟悉hook和插件化技術,其實從之前的Flutter到Native的Chanel架構就可以想到一種思路,既然解決不了Native問題,那就解決Channel的問題吧,Native端側的IPC方式無法實現,換到Flutter側和Native側的Channel通信側去實現IPC吧。參考業務對於插件化hook機制/IPC機制的理解,結合自身對於flutter channel的理解,可以實現一種利用socket服務去hook method channel和event channel實現方式,去代理客戶端的method channel和event channel,將處理結果通過socket交給服務端去處理拿到服務端真正的method channel和event channel數據即可,這才是我心中想要的實現方式就是如此,整個架構圖如下:

閒魚對Flutter-Native混合工程解耦的探索

客戶端是一臺手機,服務端也是一臺手機,服務端跑閒魚FWN主工程,客戶端跑一個乾淨的Flutter工程;客戶端先通過Flutter側代碼去找使用本端有對應的Channel,如果有則使用返回結果,如果沒有則通過Socket請求結果到服務端主工程上,主工程根據Socket定義的協議字段去解析然後發起一個channel拿結果,之後通過socket將解決返回給客戶端,客戶端拿到了socket結果數據後執行想要的渲染方式即可;

或許你有質疑點:比如為什麼要用2臺手機,使用一臺不可以麼?這裡我推薦使用2臺手機有如下2個原因:

  • 一臺手機運行2個APP,如果server在後臺可能會導致進程資源被回收,Socket通信中斷;

  • 使用2臺手機有一個極大好處是,你運行Android的Flutter側Client代碼,但是往往你需要驗證Native側雙端Server代碼數據,如果客戶端手機/服務端手機是2臺,只需要改下客戶端的IP地址去請求Android手機的Server還是IOS手機的Server就可以驗證結果;

嘗試驗證

比如如下的method channel代碼如下:

  1. <code>Future invokeMethod(Stringmethod, [dynamicarguments ])async{/<code>

  2. <code>assert(method != );/<code>

  3. <code>finalByteDataresult =awaitbinaryMessenger.send(/<code>

  4. <code>name,/<code>

  5. <code>codec.encodeMethodCall(MethodCall(method, arguments)),/<code>

  6. <code>);/<code>

  7. <code>if(result == ) {/<code>

  8. <code>throwMissingPluginException('No implementation found for method $method on channel $name');/<code>

  9. <code>}/<code>

  10. <code>finalT typedResult = codec.decodeEnvelope(result);/<code>

  11. <code>returntypedResult;/<code>

  12. <code>}/<code>

修復result == 的場景,如果是我們指定的客戶端,則通過socket去拿server數據,重點理解Fish MOD:START到Fish MOD:END代碼思想就理解了;

  1. <code>Future invokeMethod(Stringmethod, [dynamicarguments])async{/<code>

  2. <code>assert(method != );/<code>

  3. <code>finalByteDataresult =awaitbinaryMessenger.send(/<code>

  4. <code>name,/<code>

  5. <code>codec.encodeMethodCall(MethodCall(method, arguments)),/<code>

  6. <code>);/<code>

  7. <code>if(result == ) {/<code>

  8. <code>//Fish MOD:START/<code>

  9. <code>//throw MissingPluginException(/<code>

  10. <code>// 'No implementation found for method $method on channel $name');/<code>

  11. <code>//socket從服務端手機獲取值/<code>

  12. <code>finaldynamicserverData =/<code>

  13. <code>awaitSocketClient.methodDataForClient(clientParams);/<code>

  14. <code>//Fish MOD:END/<code>

  15. <code>}/<code>

  16. <code>finalT typedResult = codec.decodeEnvelope(result);/<code>

  17. <code>returntypedResult;/<code>

  18. <code>}/<code>

最後通過此種方式驗證了
MethodChannel/EventChannel數據正常收發的可行性,後續還需要在業務場景具體實驗耕田。

效果對比與後續

閒魚對Flutter-Native混合工程解耦的探索

無法方案1和方案2最終都可以解決編譯運行時長的問題,但方案1在拆分模塊和維護模塊時候都有很高的成本,運行時長雖然降低了,但是模塊化工作量卻加大很多,方案2可以完美解決拆分成本和維護成本,但是不足之處就是運行環境苛刻,可操作性不足,其需要2部手機作為運行環境,另針對於一些頁面跳轉邏輯,可能客戶端手機A觸發到服務端手機B上,操作性不在同一臺手機上;當然方案二雖然有一定缺陷,卻可以解決很多問題,因此後續在閒魚模塊化拆分落地項目中,在思考是否有更加完美的解決方法。

閒魚對Flutter-Native混合工程解耦的探索


分享到:


相關文章: