为了账号安全,请及时绑定邮箱和手机立即绑定

开源|Fair热更新设计与实现

标签:
Android Html5 iOS

原创 李昊 58技术 2021-09-16 08:47

● 项目名称:Fair 2.0

● Github地址:https://github.com/wuba/fair

● 项目简介:Fair是为Flutter设计的动态化框架,可以通过Fair Compiler工具对Dart源文件的转化,使项目获得动态更新Widget的能力。Fair 2.0是为了解决 Fair 1.0版本的“逻辑动态化”能力不足。

业界热更新现状

由于客户端每次更新的时候,都需要经过发布平台的审核,不管是需要紧急修复的bug,还是想要快速上线的需求,都绕不过审核这个问题。而很多开发者都会遇到一个问题,就是审核时间过长。审核过长可能会导致bug不能及时修复,严重者造成巨大的经济损失。也可能导致新的业务无法及时上线,错失业务的最佳推广时机。所以,长久以来,客户端都存在一个热门的话题,那就是热更新。

热更新的需求由来已久,相应的各种解决方案也非常之多。其中有些已经开源,有些还在公司内部使用,还有些已经被平台封禁。对于热更新打击最为严重的,目前来说就是苹果商店,所以不论任何热更新方案,都会优先去考虑是否可以经过苹果商店的检测。

  • iOS开发的OC语言本身是可以实现动态化的,而且在开发中又可以使用动态库去更新所需要执行的代码,看起来实现动态化很简单。可是苹果把动态化和app做了一个绑定签名,如果是没有和app同时进行签名的动态库,就没办法进行加载,所以这条最简单的路就被堵死了。

  • iOS7的时候苹果推出了JavaScripCore,这个框架作为JS和native之间的桥梁,帮助我们在JS和native进行通信,而这时候,通过JS来实现动态化的思路就出现了,其中最亮眼的就是JSPatch。JSPatch的具体思路就是在js和native之间做一个反射,从而使下发的内容可以转换成指定的类和方法等,继而通过OC的runtime来执行。由于这种方案对代码的入侵及小,并且因为JSCore的存在,接入成本也特别低,所以当时很大一批动态化都是用它来实现的。可惜好景不长,很多接入了JSPatch的开发者收到了来自苹果的责令整改的邮件,使得盛极一时的JSPatch地位降了下来。不过目前还是有很多公司在使用这种思路来进行一些热修复的实现。

  • 滴滴的动态化方案DynamicCocoa。DynamicCocoa是从编译阶段入手,通过clang编译器把OC代码编译成指定格式的JS代码,再下发下去执行,做到原生开发,动态执行的思路。手机QQ也有个方案和它比较像,不过更加一步到位,它做了一个自己的虚拟机,然后把OC代码转成了自己的字节码,在自己的虚拟机上运行。

  • ReactNative实现的动态化,目前用到的公司也是比较多的,它是通过下发bundle,从而执行最新的内容,有关native部分的api,需要开发者自己去定制bridge,和客户端的发版比较像,只是少了过审的这个过程。

以上的几种热更新的方式,基本涵盖了目前业界几种热更新的主流思想,而Fair,则是在权衡了各种方案的优劣,最终研发出来的产物。

Fair与当前已存在的热更新方案的区别

  • Fair是在flutter的基础上开发的,flutter是一个跨平台的移动UI框架,它使用高性能的自带渲染引擎进行渲染,渲染速度和用户体验堪比原生。由于它是跨平台框架,所以在开发的过程中,只需要开发一套代码,就可以实现在双端运行,开发成本大大的减少了。热更新往往伴随这需求的快速迭代,而开发成本降低,开发速度变快,更加符合一部分急需热更新的业务。

  • flutter本身是开源的,相比于原生来说,开源就意味着更高的可扩展性和可定制型。这也就使得我们在将flutter源码转换成所定制代码的时候,非常方便。

  • 做过RN的同学都知道,在各种能力已经完备的情况下,RN的开发速度是很快的,但是前期打基础的时候,势必会减慢整体的研发节奏。而flutter本身的设计就使得它可以做到快速的接入,因此相应的Fair接入的成本也会控制在一个很低的水平。

总的来说,Fair和DynamicCocoa一样,从编译阶段入手,把源代码编译成指定格式的JSON文件以及JS文件,JSON文件里面的内容记录了将要展示的Fair页面,而JS文件里面的内容则是页面的逻辑代码,而且不需要我们自己去实现一个虚拟机,也就意味着当flutter有更新的时候,不需要花大精力去修改虚拟机做适配。而Fair并不依赖naitve原生的动态化能力,也就是说对比JSPath等方案,它极大的减少了被苹果拒审的可能,使得开发者开发起来更加安全。而Fair更新新版本的操作,又和RN的下发bundle比较相似,但是接入成本远远低于RN,并且自带的skia渲染引擎所带来的用户体验也是相差极大的。

Fair热更新整体流程

我们的flutter代码从编写到动态的运行到我们的手机App上,大体上经历了这么几个过程。

  • flutter源码经过编译,生成JSON,JS代码文件,这些文件会被打包成一个zip文件,从而减少传输过程中的消耗。在编写flutter代码的时候,需要对一些代码进行标记,这样在编译的过程中就会根据不同的标记情况,将我们编写的flutter代码分别编译成JSON和JS。

  • 打包好的zip包传输到服务器上,根据不同的版本一一进行管理,并且可以随时控制用户当前所需要展示的版本,当有线上问题的时候,可以随时由服务控制切换到指定的版本。

  • 客户端通过对版本的校验,获取到服务器想要展示的版本,然后下载对应的zip包,在客户端对文件进行一系列校验,以及本地的版本切换操作。

  • 本地Fair读取最新的文件,通过FairWidget对文件的解析,从而达到热更新的目的。

https://img2.sycdn.imooc.com/62d52387000182a302470519.jpg

以上的一整套流程中,有关Fair源码编译生成JSON和JS文件的zip包,以及读取JSON和JS文件从而在手机上展示页面和处理逻辑这两部分,都可以在之前的文章中找到实现的方案。剩下的内容,开发者可以根据需要来自己进行定制。

但是,为了更方便开发者快速接入Fair,并且方便后续我们对整个Fair流程管理的优化,Fair_api项目应运而生,目前它实现了最基本的流程管理工作,不强制开发者去使用,在后面会不断进行优化和扩充,在给开发者最大自由度的同时,提供更多的能力,方便Fair的接入。

Fair热更新的设计思路

  • Fair的设计是组件级的热更新,可以和flutter原生组件同时存在。之所以这样设计,主要是考虑到方便当前已有项目的接入,只需要对需要热更新的组件进行热更新处理,对不需要热更新的位置没有任何影响,简单快速,可以说是Fair设计的原则之一。并且,对于flutter来说,每个页面也可以看成一个组件,所以,页面级的热更新也是可以用Fair来进行实现的。

  • Fair的动态化分为UI部分和逻辑部分,UI部分是将flutter代码编译成JSON格式的内容,最终还是交给flutter原生进行加载,而逻辑部分则被转换成了JS代码。这样设计的原因有两个,首先,如果选择自己去实现一个虚拟机去执行flutter编译出来的定制格式的代码,那么就要考虑到后续如果flutter新增或者修改了语法糖,那么虚拟机和编译工具就要立刻进行修改,一旦没有及时进行适配,就会导致和最新的flutter引擎不匹配,影响开发者的开发。其次,Fair更倾向于无侵入的方式来实现热更新,因为这样,就会更好的享受到flutter更新带来的各种优化效果。

  • 当次更新代码,下次启动生效。这样设计的原因主要是为了让用户有更加流畅的体验,在用户完全无感知的情况下进行版本的更新。并且一旦在文件下载或者处理过程中出现问题,也不会出现页面一直loading的问题,因为这些都是在用户无感知的情况下处理的。

  • 有关版本控制,Fair更加推荐开发者把它放到服务去处理,所以在客户端只做了校验的工作,这是为了让客户端这边更加专注于业务,并且既然是热更新,那么本身就是要加强服务对客户端的控制,以便及时的发现和处理问题。

Fair热更新流程

①首先是编写一个纯flutter代码的widget,该widget支持传入两个参数,这两个参数在使用Fair组件的时候和Fair的JSON文件路径一起传入的,其中Sugar.ifEqualBool是一个Fair的语法糖,它的作用是在当前flutter代码进行转换的时候,会把它里面的代码转换成JS文件去做逻辑的运算,也就是说,下面代码中的_countCanMod2方法中的逻辑判断部分会被转换成JS。

// 逻辑方法
  bool _countCanMod2() {
      return _count % 2 == 1;
  }
  @override  Widget build(BuildContext context) {
      return Scaffold(      
          appBar: AppBar(
                  title: Text(_title),      
                  ),      
          body: Center(
                  child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,          
                            children: [            
                            // Sugar.ifEqualBool 为逻辑和布局混编场景下的语法糖            
                            Sugar.ifEqualBool(_countCanMod2(),                
                            falseValue: Image.asset(\'assets/image/logo.png\'),                
                            trueValue: Image.asset(\'assets/image/logo2.png\')),            
                            Padding( 
                                         padding: EdgeInsets.only(top: 20),
                                         child: Text(\'_count = $_count\'), 
                                   ),            
                            Padding(
                                          padding: EdgeInsets.only(top: 20),              
                                          child: Text(\'if _count % 2 == 1,  
                                          update image !\'),            
                                    ),          
                           ],        
                   ),      
           ),   
     );
   }

②执行flutter pub run build_runner build命令,会在控制台打印出当前指令所生成的文件,消耗的时间,以及最终的编译结果,如果编译失败,也会展示对应的错误信息,开发者只需要根据错误信息去修改即可。

③这时候编译的产物就可以作为下发的文件了,按照上面Fair整体流程中的内容,这时候就可以把文件交给服务端去做处理了,服务端可以把当前的版本号和文件做一个简单的对应关系。

④客户端这个时候在启动app的话,可以选择去请求服务端,判断当前是否有新的版本,如果有,则去拉取最新的文件,也就是把我们第②中生成的内容拉取到客户端,然后在客户端中把对应路径传入到FairWidget中,组件就可以运行出来了。

https://img2.sycdn.imooc.com/62d5240e0001644b04681001.jpg

Fair-API的使用和实现

为了方便开发者进行开发,让开发者的精力尽量都投入到业务中去,Fair_API应运而生,它是为了解决文件从服务端到展示在用户手机上这一流程,它主要是对文件处理流程和校验进行管理。依托于Fair项目,但是Fair项目并不依赖它,目前可以作为一个插件进行使用,后面在Fair_API相对稳定的时候会合并到Fair中。目前并不强制用户使用,用户也可以根据自己的业务去进行定制化的开发,给与用户最大的自由度。

1、在项目中的pubspec.yaml文件中直接引入Fair_API的最新版本。

Fair_API:  git:    url: git@igit.58corp.com:android-lab/Fair_API.git    ref: master

2、在合适的时机调用下载文件的方法downloadBundle,并传入对应的url,即可完成异步下载文件。

FairApi.downloadBundle("https://www.58.com/Fair");

3、在使用Fair的时候,需要传入对应widget的文件路径,这里调用Fair_API中的获取文件路径的方法filePath,并传入文件名,就会返回对应的文件路径,filePath可以用户自己配置,在不使用fair_api_path的情况下,会展示filePath路径下对应的内容,在使用fair_api_path的情况下,filePath则可作为兜底。


class _State extends State<SamplePluginPage> {
  String filePath = \'\';
  @override  void initState() {
      // TODO: implement initState    super.initState();
        }
   @override  Widget build(BuildContext context) {
       return MaterialApp(
             home: Scaffold(
                     body: FairWidget(
                               name: \'逻辑动态页面\',
                                         path: filePath,
                                                   fair_api_path: FairApi.filePath("lib_src_page_logic-page_sample_logic_page")
                 )
            ),
       );
}}

Fair-API的一些底层实现

1、获取文件路径的流程:根据传入的文件会优先从本地的文件路径中进行查找,如果存在就直接返回对应的路径,如果没有的话则会去做一个网络请求,为了防止同时请求,这里面会有个防止同时多次请求的处理。

https://img2.sycdn.imooc.com/62d5247c0001ef5d07490787.jpg

2、文件下载的流程:首先是需要两次网络请求,第一次获取最新的版本号和最新的zip包地址,第二次网络请求则是去下载对应的包,然后在本地进行解压处理,解压到指定的中间文件中,一切处理好之后,下次在启动app的时候,则会取最新的地址进行返回。下次启动app的时候,会先去中间文件夹查看是否有解压成功的中间,如果有则去做一个md5完整性校验,如果没有则去看是否有未解压的压缩包,如果有则再走下解压md5校验流程,再在下次生效。如果下载的文件均有问题,并且本地也没有完整的文件,也没有兜底的文件打包进包里,那么Fair会使用自己的兜底页面进行展示,防止出现白屏的情况。

https://img1.sycdn.imooc.com/62d524960001374207500707.jpg

补充说明

1、本地不做有关版本的任何逻辑处理,只是保存,如果需要回滚代码,只需要在服务指定返回的下载文件url即可。

2、更新时,下载的包中有多少个文件就覆盖多个文件,其他的文件不变。

3、有关缓存,目前使用的是flutter自带的读取文件的缓存,已经足够满足Fair的需求。

4、服务端的配置需要给客户端返回两个参数,分别是当前的版本号bundleVersion和下载的压缩包的链接binUrl参数。

5、fair_api_path是和fair_api配套使用的,里面封装了包括路径读取、widget刷新、容错等一系列处理,如果用户想要自己制定路径,使用filePath即可。

总结

Fair热更新吸取了当前业界各种热更新方案的经验,并在跨平台框架flutter的基础上搭建出来,拥有着接入简单,开发快速,侵入性低,时效性高等特点,并且相配套的流程管理工具也在不断的完善当中与改进当中,相信配合flutter的各种特性,未来必然成为跨平台热更新方案的首选之一。

谢谢大家!

交个朋友,帮我们点个star吧 🌟  😇:Github地址:https://github.com/wuba/fair





点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消