开源 | Fair 在 58 同城拍客 App 中的实践

2,942 阅读9分钟

本文旨在为大家提供 Fair 在实际项目中落地的完整案例,包含了使用 Fair 进行动态页面改造、复杂场景使用、接入过程中遇到的问题、Fair 接入前后的性能对比、热更新方案设计等等内容。

本文重在介绍 Fair 的落地,不会涉及过多的技术原理,如果对 Fair 原理感兴趣的同学,可以阅读以下文章:

本文目录

1. Fair 接入和使用流程

1.1 接入 Fair

对于 Fair 的接入方式,推荐大家使用 源码依赖 的方式,下载地址如下:

git clone https://github.com/wuba/fair.git

pubspec.yaml 文件里添加依赖:

假如 Fair 源码和你的项目代码在同一个目录

另外需要特别注意的是,Fair 内部通过 fair_version 库来做版本控制,所以大家需要根据自己本机的 Flutter SDK 版本来决定 fair_version 的版本。

以我的电脑为例子,我本地使用的 Flutter SDK 版本为 2.5.0,所以我强制指定 fair_version 的版本为 flutter_2_5_0

1.2 将 Widget 改造为可动态下发的 bundle

将一个 Widget 改造为可以动态下发的 bundle 需要三步。

第一步:改造 main 函数

需要去掉默认的 runApp(MyApp()),然后手动调用一下 WidgetsFlutterBinding.ensureInitialized(),最后用 FairApp 来包裹项目的根 Widget。

第二步:添加 @FairPatch() 注解

在需要改造的 Widget 上添加 @FairPatch() 注解。

第三步:执行 build_runner 命令

执行了 build_runner 即可生成 bundle 资源

flutter pub run build_runner build

1.3 bundle 资源生成位置和组成

bundle 资源的位置,位于:project -> build -> fair 下。

4dda50cabd0c0c014286b8c42b6c67c2.png

一个 bundle 资源包含了两个文件:一个是 js 文件,一个是 json 文件

  • js 文件里面包含的是动态页面的逻辑部分
  • json 文件包含的是动态页面的布局 DSL

1.4 使用 FairWidget 加载 bundle 资源

bundle 资源生成好以后,我们可以使用 FairWidget 组件对其进行加载。

在本地测试时,我们可以将 bundle 资源拷贝到 assets 目录下,然后将 assets 资源的地址通过 path 参数传递给 FairWidget:

运行效果如下:

本地测试通过以后,可以将 bundle 资源托管到自己的服务器上,然后将 path 换成一个 bundle 资源的服务器下载地址即可:

使用小结

到这里简单小结一下 Fair 的使用步骤:

对于一个简单的页面可以通过以上步骤进行改造,但是实际的项目中,情况往往比较复杂,所以,我们下面继续介绍在一些复杂的业务场景下,如何使用 Fair。

1.5 动态页面之间的参数传递

Fair 提供了两个注解,可以实现动态页面之间的参数传递:

  • @FairWell
  • @FairProps

这两个注解的区别是:

  • @FairWell 注解传递的参数不能参与逻辑运算,而 @FairProps 传递的参数可以参与逻辑运算。

  • @FairWell 支持 Dart 语言的所有数据类型,而 @FairProps 只支持传递一个 Map

来看看示例代码。

@FairWell 的使用:

@FairProps 的使用:

1.6 动态页面之间的跳转

推荐使用 命名路由 的方式进行跳转。

首先注册路由表:

然后使用 Navigator.pushNamed 进行跳转:

1.7 动态页面引用本地 Widget 组件:@FairBinding

对于一个本地的 Widget,如果直接在动态页面里面引用的话,Fair 无法完成 DSL 的生成,需要使用 @FairBinding 注解来对本地 Widget 组件进行标记。

标记完成后,可以运行 flutter pub run build_runner build 命令,运行成功后,会在项目的 src 目录下,生成本地 Widget 的组件映射表:AppGeneratedModule

我们需要对 AppGeneratedModule 进行注册:

然后就可以在动态页面里使用了:

1.8 动态页面引用三方 sdk 的 Widget 组件:@FairBinding(package)

那很多人可能会有疑问了,假如我引用的是一个第三方 SDK 里的 Widget 呢?

我们依然使用的是 @FairBinding 注解,不过需要我们将三方 SDK 的 Widget 的 package 路径传递给 FairBinding:

1.9 逻辑模版使用:FairDelegate

对于页面中一些固定的逻辑方法,我们可以将其抽离出来,放到 FairDelegate 里面,来降低 Dart 与 JS 的频繁通信,达到提升性能的目标。

甚至一些页面,只需要 UI 动态化,而不需要逻辑动态化,那么完全可以使用 FairDelegate 来实现。

使用 FairDelegate 方式很简单,首先自定义一个类,让它继承 FairDelegate:

然后,需要在 FairApp 里注册模板:

1.10 使用 IFairPlugin 桥接常用第三方 SDK 的能力

我们在动态页面中,经常会用到一些第三方 SDK,比如网络请求、权限申请、拍照功能等等。

此时,我们需要使用 IFairPlugin 来桥接第三方 SDK 的能力,才能在动态页面中正常使用。

我们以权限申请库为例子。

首先,自定义一个类,继承 IFairPlugin

重写 getRegisterMethods(),在里面注册需要暴露给 JS 侧的方法。

同样的,定义好 Plugin 后,需要在 FairApp 里进行注册:

然后就可以正常使用了。

2. Fair 在 58 拍客中的落地

Fair 上线后,我们也是在 58 拍客里进行了接入和使用。

我们完成了三个页面的改造:发布页面、视频列表页面、视频详情页面。

并且自己设计了热更新流程,下面,我们就来详细介绍一下。

2.1 58 拍客接入 Fair 前后 UI 对比

接入 Fair 前的页面:

接入 Fair 后的页面:

Fair 能做到像素级别的还原,因此接入前后,页面几乎看不出任何差异。

2.2 更新方案设计

因为目前 Fair 的热更新平台正在开发中,还没有上线,因此,我们自己设计了一套热更新方案:

  • 方案1:同步更新
  • 方案2:静默更新
  • 方案3:预加载+同步更新
  • 方案4:预加载+静默更新

2.2.1 同步更新方案

采用同步更新方案时,每次进入页面会先请求版本号,如果有更新,则下载 bundle 资源(如已有缓存则不下载),最后展示。

流程图如下:

2.2.2 静默更新方案

采用静默更新方案,每次进入页面时,如有缓存则直接展示,并异步更新 bundle 资源。如无缓存,则使用同步更新。

所以,静默更新有 3 个特点:

2.2.3 预加载

对于标记为预加载的资源,我们会在 APP 启动的时候就下载好 bundle 资源。

比如,我们改造 58 拍客的视频列表页,它位于首页的第二个 tab,对于这样的 tab 页,我们当然是希望它在 APP 启动时就进行预加载。

预加载的方案一般不会单独使用,会选择与同步更新和静默更新一起使用。

总结

2.3 如何保证 bundle 资源安全性?

如果我们在下载 bundle 资源的过程中,因为一些未知原因导致资源未下载完整,或者说下载了一个被恶意替换的资源,此时,去加载这样一个不合法资源的话,会发生一些未知的错误,增加 APP 的风险。

那么如何解决呢?

我们的解决方案是采用验证 校验和 的方式。

我们将 bundle 资源上传到服务器时,会通过 MD5 或 SHA-256 计算出 bundle 资源的 校验和(Checksum),并由热更新接口下发。

客户端完成 bundle 下载后,重新计算一遍得出校验和,如果与服务端下发一致,则为完整且合法的,可以执行加载。

特别注意:上图中展示的校验和是明文的形式,实际开发中,需要大家对其进行加密后再下发。这样做是为了进一步提升数据的安全性。

比如拍客里面,是对其进行了非对称加密后下发,客户端解密后再使用。

2.4 兜底策略(防止未知错误)

在拍客里,我们加入了一个兜底的策略,即:由 Server 端控制加载 bundle 资源还是 Flutter 原生的 Widget。

伪代码如下:

这样做的目的是,当线上发生一些未知的错误而无法纠正时,能够及时控制。

2.5 Fair 落地过程中遇到的问题

第一个问题:图片模糊

如上图所示,我们在完成改造后,加载页面发现图片变得非常模糊。

之所以会有这个问题是因为,Fair 将 DSL 转化为 Widget 时,默认取的是 assets 目录下的 1x 图,所以导致了在一些高分辨率手机下出现模糊。

解决方案是,1x 图不要使用低分辨率图片,且 Image 需要设置 widthheightfit 属性。

第二个问题是:colors 属性缺失

我们在代码里设置了一个颜色值:Colors.red[50]

但是生成 bundle 资源后,发现 DSL 里并没有对于的 color 属性:

出现这个问题的原因是,Fair 暂时不能支持解析 Colors.xx[xx] 这种设置颜色的深度的语法。

推荐使用十六进制颜色值 Color(0xAARRGGBB)的写法。

3. 接入 Fair 前后性能对比

首先同步一下开发环境和测试环境:

开发环境:

  • macOS Big Sur 11.2(Apple M1)
  • Flutter SDK 2.5.0

测试设备:

  • HUAWEI nova 7,Android 11
  • iOS iPhoneXSMax,iOS13.3

测试方式:

  • 非压测,模拟用户正常使用
  • 模拟了12个事件

测试页面:

3.1 页面流畅度

通过模拟 12 个操作事件,如滑动、点击、跳转来进行测试。

3.2 内存表现(增量数据)

接入 Fair 后,Android 端的内存占用 增加了 22MB,iOS 端增加了 17.9MB。

3.3 启动时间(增量数据)

接入 Fair 后,Android 端的启动时间 增加了 0.03 秒,iOS 端增加了 0.1 秒。

4. Fair 开发体验总结

我们在 58 拍客里接入和使用 Fair 后,对 Fair 有 3 个比较大的感受:

第一个感受是:保留Flutter原生开发习惯

使用 Fair 不需要学习新的技术栈,也不用适应新的语法习惯,只需要按照 Flutter 原生的写法开发即可。

第二个感受是:改造简单

将 Widget 改造为动态页面,只需要加上几个注解即可,方便、实用。

参考数据:58 拍客接入 Fair + 3 个页面改造排期: 2 天

第三个感受是:bundle 资源大小控制合理

58 拍客改造了 3 个页面,这 3 个页面总的 bundle 资源大小才 8KB

最后,欢迎大家使用 Fair,也欢迎大家为我们点亮 Star

git clone https://github.com/wuba/fair.git

作者简介:

陈有余:58 同城,Android 高级开发工程师