41

闲鱼对Flutter-Native混合工程解耦的探索

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU4MDUxOTI5NA%3D%3D&%3Bmid=2247485720&%3Bidx=1&%3Bsn=45c4b95fbbd8be0ce2d6236f792edf72
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

闲鱼Flutter现状

闲鱼是第一个使用Flutter混合开发的大型应用,但闲鱼客户端开发最深入体会的痛点就 是编译时长影响开发体验。在Flutter+Nativ e这种开发模式下,Native编译速度慢,模块开发无法突破。闲鱼集成了集团众多中间件,很多功能无法通过flutter直接调用,需使用各种channel到native去调用对应功能。总而言之,闲鱼目前Flutter开发面临如下几个痛点:

  • Flutter侧混合编译速度慢,Android首次编译 10min+ ,iOS首次编译 20min+

  • 混合栈编程中历史包袱导致IOS/Android双端返回给Flutter侧的数据可能存在 不一致性

  • 集成模块 开发效率相比模块开发较低 ,单模块页面测试性能数据无法展开;

方案一:模块化开发

方案概述

此项目从立项至今已经很长一段时间,由于业务迭代快,native插件满天飞情况下,想要做到工程模块化拆分难度可想而知;如下图是项目立项为模块化拆分,业务方需要将各个业务拆分解耦合,拆分集团中间件,业务封装组件,Native业务代码,Flutter桥代码,Flutter组件库,Flutter侧业务代码等多个模块;项目初衷就是整理代码,提供一个Flutter可运行的干净环境,同时需要让flutter可以获取到native几乎所有能力,但是编译开发调试时候有想要速度快,效率高。能想到的最直接解决方案就是拆包,从0-1建立一个最小壳工程,然后拆分集团基本中间件,封装业务组件,Flutter插件等,如下是整个项目架构:

iyeYJbE.png!mobile

日常模块化单页面级需要使用最小壳工程,其内部有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获取,因此可以换一种方式思考?

6b2UZvm.png!mobile

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数据即可,这才是我心中想要的实现方式就是如此,整个架构图如下:

VfUfmm7.png!mobile

客户端是一台手机,服务端也是一台手机,服务端跑闲鱼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. Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) async{

  2. assert(method != null);

  3. finalByteData result = await binaryMessenger.send(

  4. name,

  5. codec.encodeMethodCall(MethodCall(method, arguments)),

  6. );

  7. if(result == null) {

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

  9. }

  10. final T typedResult = codec.decodeEnvelope(result);

  11. return typedResult;

  12. }

修复result == null的场景,如果是我们指定的客户端,则通过socket去拿server数据,重点理解Fish MOD:START到Fish MOD:END代码思想就理解了;

  1. Future<T> invokeMethod<T>(String method, [dynamic arguments]) async{

  2. assert(method != null);

  3. finalByteData result = await binaryMessenger.send(

  4. name,

  5. codec.encodeMethodCall(MethodCall(method, arguments)),

  6. );

  7. if(result == null) {

  8. //Fish MOD:START

  9. //throw MissingPluginException(

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

  11. //socket从服务端手机获取值

  12. finaldynamic serverData =

  13. awaitSocketClient.methodDataForClient(clientParams);

  14. //Fish MOD:END

  15. }

  16. final T typedResult = codec.decodeEnvelope(result);

  17. return typedResult;

  18. }

最后通过此种方式验证了MethodChannel/EventChannel数据正常收发的可行性,后续还需要在业务场景具体实验耕田。

效果对比与后续

UvM7jmR.png!mobile

无法方案1和方案2最终都可以解决编译运行时长的问题,但方案1在拆分模块和维护模块时候都有很高的成本,运行时长虽然降低了,但是模块化工作量却加大很多,方案2可以完美解决拆分成本和维护成本,但是不足之处就是运行环境苛刻,可操作性不足,其需要2部手机作为运行环境,另针对于一些页面跳转逻辑,可能客户端手机A触发到服务端手机B上,操作性不在同一台手机上;当然方案二虽然有一定缺陷,却可以解决很多问题,因此后续在闲鱼模块化拆分落地项目中,在思考是否有更加完美的解决方法。

RJ3aeiB.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK