作者:祈晴
1. 閑魚Flutter現狀
閑魚是第一個使用Flutter混合開發的大型應用,但閑魚用戶端開發最深入體會的痛點就是編譯時長影響開發體驗。在Flutter+Native這種開發模式下,Native編譯速度慢,子產品開發無法突破。閑魚內建了集團衆多中間件,很多功能無法通過flutter直接調用,需使用各種channel到native去調用對應功能。總而言之,閑魚目前Flutter開發面臨如下幾個痛點:
- Flutter側混合編譯速度慢,Android首次編譯10min+,iOS首次編譯20min+;
- 混合棧程式設計中曆史包袱導緻IOS/Android雙端傳回給Flutter側的資料可能存在不一緻性;
- 內建子產品開發效率相比子產品開發較低,單子產品頁面測試性能資料無法展開;
2.解決方案一
2.1方案概述
此項目從立項至今已經很長一段時間,由于業務疊代快,native插件滿天飛情況下,想要做到工程子產品化拆分難度可想而知;如下圖是項目立項為子產品化拆分,業務方需要将各個業務拆分解耦合,拆分集團中間件,業務封裝元件,Native業務代碼,Flutter橋代碼,Flutter元件庫,Flutter側業務代碼等多個子產品;項目初衷就是整理代碼,提供一個Flutter可運作的幹淨環境,同時需要讓flutter可以擷取到native幾乎所有能力,但是編譯開發調試時候有想要速度快,效率高。能想到的最直接解決方案就是拆包,從0-1建立一個最小殼工程,然後拆分集團基本中間件,封裝業務元件,Flutter插件等,如下是整個項目架構:

日常子產品化單頁面級需要使用最小殼工程,其内部又channel的聲明和實作,通過運作最小殼工程運作得到結果,Flutter側子產品開發通過IOC調用到最小殼工程的channel得到傳回結果,最後将子產品化開發以一種pub或者git依賴方式內建到閑魚FWN主工程即可;
2.2 階段性産出
業務子產品化拆分從來都是一種吃力不讨好的活,明知道拆出來有收益,但是投入産出比不足,是以曆史包袱代碼越來越厚重,以至于下一個接收的人都不敢輕易修改代碼;在子產品化拆分時候,開始項目時候提出過新起一個幹淨的工程,然後一步步拆分集團中間件,期間拆出了Mtop/Login/FlutterBoost/UI Plugin,耗時3周/2人,得到部分結果就是新業務,新界面開發滿足基本快速疊代開發,缺點也很明顯如下所示:
- 拆分梳理Native的中間件繁瑣,工作量巨大,最小化殼工程耗時3周/2人
- 推動業務方拆分基礎元件庫更難,目前項目進展不順
- 維護成本高,拆分殼工程運作結果和主工程可能不一緻
- 業務迫切其結果,但投入産出比不足,比如Flutter單頁面性能測試,Flutter側子產品化拆分,Fass工程一體基石
3.解決方案二
3.1 換位思考
(1)若自己是業務方,需要為Flutter側去拆分包,去建構一個最小化殼工程,其成本是巨大的。
(2)Fass工程一體化依賴一個最小化殼工程的Native運作環境去運作Flutter側代碼,可是并非所有的業務方都會提供一個最小化殼工程去運作Fass,那麼Fass工程一體化/子產品開發如果在集團其他運作環境下進展?
(3)最小化殼工程運作環境無法緊跟Native側的各種版本,會導緻運作結果不一緻情況下也不敢随便使用;
如果解決此問題呢?個人提出過跨程序實作方式,在Android端側跨程序調用實作方式一直很常見的場景,client通路server得結果,而Flutter側和Native側不就是client和server雙端麼?如下圖所示,其實Flutter擷取資料就是通過MethodChannel/EventChannel擷取,是以可以換一種方式思考?

3.2 IPC跨程序通信,Android Binder
期間在Android側我使用過Android Binder去實作,新起一個APP做為殼工程,其内部實作了各種插件去通路主工程服務,擷取結果然後傳回給殼工程的Flutter調用,但是維護成本依然在;同時iOS側沒有對應的實作機制,是以此方式被抛棄;
3.3 具體方案: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資料即可,這才是我心中想要的實作方式就是如此,整個架構圖如下:

用戶端是一台手機,服務端也是一台手機,服務端跑閑魚FWN主工程,用戶端跑一個幹淨的Flutter工程;用戶端先通過Flutter側代碼去找使用本端有對應的Channel,如果有則使用傳回結果,如果沒有則通過Socket請求結果到服務端主工程上,主工程根據Socket定義的協定字段去解析然後發起一個channel拿結果,之後通過socket将解決傳回給用戶端,用戶端拿到了socket結果資料後執行想要的渲染方式即可;
或許你有質疑點:比如為什麼要用2台手機,使用一台不可以麼?
這裡我推薦使用2台手機有如下2個原因:
(1)一台手機運作2個APP,如果server在背景可能會導緻程序資源被回收,Socket通信中斷;
(2)使用2台手機有一個極大好處是,你運作Android的Flutter側Client代碼,但是往往你需要驗證Native側雙端Server代碼資料,如果用戶端手機/服務端手機是2台,隻需要改下用戶端的IP位址去請求Android手機的Server還是IOS手機的Server就可以驗證結果;
3.4 嘗試驗證
比如如下的method channel代碼如下:
Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) async {
assert(method != null);
final ByteData result = await binaryMessenger.send(
name,
codec.encodeMethodCall(MethodCall(method, arguments)),
);
if (result == null) {
throw MissingPluginException('No implementation found for method $method on channel $name');
}
final T typedResult = codec.decodeEnvelope(result);
return typedResult;
}
修複result == null的場景,如果是我們指定的用戶端,則通過socket去拿server資料,重點了解Fish MOD:START到Fish MOD:END代碼思想就了解了;
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
assert(method != null);
final ByteData result = await binaryMessenger.send(
name,
codec.encodeMethodCall(MethodCall(method, arguments)),
);
if (result == null) {
//Fish MOD:START
//throw MissingPluginException(
// 'No implementation found for method $method on channel $name');
//socket從服務端手機擷取值
final dynamic serverData =
await SocketClient.methodDataForClient(clientParams);
//Fish MOD:END
}
final T typedResult = codec.decodeEnvelope(result);
return typedResult;
}
最後通過此中方式驗證了MethodChannel/EventChannel資料正常收發的可行性,後續還需要在業務場景具體實驗耕田;
4.結果對比和展望
結果對比:

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