关于前端:Flutter包大小治理上的探索与实践

10次阅读

共计 14783 个字符,预计需要花费 37 分钟才能阅读完成。

一、背景

Flutter 作为一种全新的响应式、跨平台、高性能的挪动开发框架,在性能、稳定性和多端体验统一上都有着较好的体现,自开源以来,曾经受到越来越多开发者的青睐。随着 Flutter 框架的一直倒退和欠缺,业内越来越多的团队开始尝试并落地 Flutter 技术。不过在实际过程中咱们发现,Flutter 的接入会给现有的利用带来比拟显著的包体积减少。不论是在 Android 还是在 iOS 平台上,仅仅是接入一个 Flutter Demo 页面,包体积至多要减少 5M,这对于那些包大小敏感的利用来说其实是很难承受的。

对于包大小问题,Flutter 官网也在继续跟进优化:

  • Flutter V1.2 开始反对 Android App Bundles,反对 Dynamic Module 下发。
  • Flutter V1.12 优化了 2.6% Android 平台 Hello World App 大小(3.8M -> 3.7M)。
  • Flutter V1.17 通过优化 Dart PC Offset 存储以缩小 StackMap 大小等多个伎俩,再次优化了产物大小,实现 18.5% 的缩减。
  • Flutter V1.20 通过 Icon font tree shaking 移除未用到的 icon fonts,进一步优化了利用大小。

除了 Flutter SDK 外部或 Dart 实现的优化,咱们是否还有进一步优化的空间呢?答案是必定的。为了帮忙业务方更好的接入和落地 Flutter 技术,MTFlutter 团队对 Flutter 的包大小问题进行了调研和实际,设计并实现了一套基于动静下发的包大小优化计划,瘦身成果也十分可观。这里分享给大家,心愿对大家能有所帮忙或者启发。

二、Flutter 包大小问题剖析

在 Flutter 官网的优化文档中,提到了缩小利用尺寸的办法:在 V1.16.2 及以上应用—split-debug-info 选项(能够拆散出 debug info);移除无用资源,缩小从库中带入的资源,管制适配的屏幕尺寸,压缩图片文件。这些措施比拟间接并容易了解,但为了摸索进一步瘦身空间并让大家更好的了解技术计划,咱们先从理解 Flutter 的产物形成开始,而后再一步步剖析有哪些可行的计划。

2.1 Flutter 产物介绍

咱们首先以官网的 Demo 为例,介绍一下 Flutter 的产物形成及各局部占比。不同 Flutter 版本以及打包模式下,产物有所不同,本文均以 Flutter 1.9 Release 模式下的产物为准。

2.1.1 iOS 侧 Flutter 产物

iOS 侧的 Flutter 产物次要由四局部组成(info.plist 比拟小,对包体积的影响可疏忽,这里不作为重点介绍),表格 1 中列出了各局部的详细信息。

2.1.2 Android 侧 Flutter 产物

Android 侧的 Flutter 产物总共 5.16MB,由四局部组成,表格 2 中列出了各局部的详细信息。

2.1.3 各局部产物的变化趋势

无论是 Android 还是 iOS,Flutter 的产物大体能够分为三局部:

  1. Flutter 引擎,该局部大小固定不变,但初始占比拟高。
  2. Flutter 业务与框架,该局部大小随着 Flutter 业务代码的增多而逐步减少。它是这样的一个曲线:初始增长速度极快,随着代码增多,增长速度逐步减缓,最终趋近线性增长。起因是 Flutter 有一个 Tree Shaking 机制,从 Main 办法开始,逐级援用,最终没有被援用的代码,比方类和函数都会被裁剪掉。一开始引入 Flutter 之后轻易写一个业务,就会大量用到 Flutter/Dart SDK 代码,这样初期 Flutter 包体踊跃速减少,然而过了一个临界点,用户包体积的减少就根本取决于 Flutter 业务代码增量,不会增长得太快。
  3. Flutter 资源,该局部初始占比拟小,前期增长次要取决于用到的本地图片资源的多少,增长趋势与资源多少成正比。

下图 3 展现了 Flutter 各资源变动的趋势:

2.2 不同优化思路剖析

下面咱们对 Flutter 产物进行了剖析,接下来看一下官网提供的优化思路如何利用于 Flutter 产物,以及对应的艰难与收益如何。

1. 删减法

Flutter 引擎中包含了 Dart、skia、boringssl、icu、libpng 等多个模块,其中 Dart 和 skia 是必须的,其余模块如果用不到倒是能够思考裁掉,可能带来几百 k 的瘦身收益。业务方能够依据业务诉求自定义裁剪。

Flutter 业务产物,因为 Flutter 的 Tree Shaking 机制,该局部产物从代码的角度曾经是精简过的,要想持续精简只能从业务的角度去剖析。

Flutter 资源中占比拟多的个别是图片,对于图片能够依据业务场景,适当升高图片分辨率,或者思考替换为网络图片。

2. 压缩法

因为无论是 Android 还是 iOS,安装包自身曾经是压缩包了,对 Flutter 产物再次压缩的收益很低,所以该办法并不实用。

3. 动静下发

对于动态资源,实践上是 Android 和 iOS 都能够做到动静下发。而对于代码逻辑局部的编译产物,在 Android 平台反对可执行产物的动静加载,iOS 平台则不容许执行动静下发的机器指令。

通过下面的剖析能够发现,除了删减、压缩,对所有业务实用、可行且收益显著的进一步优化空间重点在于动静下发了。可能动静下发的局部越多,包大小的收益越大。因而咱们决定从动静下发动手来设计一套 Flutter 包大小优化计划。

三、基于动静下发的 Flutter 包大小优化计划

咱们在 Android 和 iOS 上实现的包大小优化计划有所不同,区别在于 Android 侧能够做到 so 和 Flutter 资源的全副动静下发,而 iOS 侧因为零碎限度无奈动静下发可执行产物,所以须要对产物的组成和其加载逻辑进行剖析,将其中非必须和动态链接库一起加载的局部进行动静下发、运行时加载。

当将产物动静下发后,还须要对引擎的初始化流程做批改,这样能力保障产物的失常加载。因为两端技术栈的不同,在很多具体实现上都采纳了不同的形式,上面就别离来介绍下两端的计划。

3.1 iOS 侧计划

在 iOS 平台上,因为零碎的限度无奈实现在运行时加载并运行可执行文件,而在上文产物介绍中能够看到,占比拟高的 App 及 Flutter 这两个均是可执行文件,实践上是不能进行动静下发的,实际上对于 Flutter 可执行文件咱们能做的的确不多,但对于 App 这个可执行文件,其外部组成的四个模块并不是在链接时都必须存在的,能够思考局部移出,进而来实现包体积的缩减。

因而,在该局部咱们首先介绍 Flutter 产物的生成和加载的流程,通过对流程细节的剖析来挖掘出产物能够被拆分出动静下发的局部,而后基于实现原理来设计实现工程化的计划。

3.1.1 实现原理简析

为了实现 App 的拆分,咱们须要理解下 App.framework 是怎么生成以及各局部资源时如何加载的。如下图 4 所示,Dart 代码会应用 gen_snapshot 工具来编译成.S 文件,而后通过 xcrun 工具来进行汇编和链接最终生成 App.framework。其中 gen_snapshot 是 Dart 编译器,采纳了 Tree Shaking 等技术,用于生成汇编模式的机器代码。

产物加载流程:

如上图 5 所示,Flutter engine 在初始化时会从依据 FlutterDartProject 的 settings 中配置资源门路来加载可执行文件(App)、flutter_assets 等资源,具体 settings 的相干配置如下:

// settings
{
...
  // snapshot 文件地址或内存地址
  std::string vm_snapshot_data_path;  
  MappingCallback vm_snapshot_data;
  std::string vm_snapshot_instr_path;  
  MappingCallback vm_snapshot_instr;

  std::string isolate_snapshot_data_path;  
  MappingCallback isolate_snapshot_data;
  std::string isolate_snapshot_instr_path;  
  MappingCallback isolate_snapshot_instr;

  // library 模式下的 lib 文件门路
  std::string application_library_path;
  // icudlt.dat 文件门路
  std::string icu_data_path;
  // flutter_assets 资源文件夹门路
  std::string assets_path;
  // 
...
}


以加载 vm_snapshot_data 为例,它的加载逻辑如下:

std::unique_ptr<DartSnapshotBuffer> ResolveVMData(const Settings& settings) {
  // 从 settings.vm_snapshot_data 中取
  if (settings.vm_snapshot_data) {...}
  
  // 从 settings.vm_snapshot_data_path 中取
  if (settings.vm_snapshot_data_path.size() > 0) {...}
  // 从 settings.application_library_path 中取
  if (settings.application_library_path.size() > 0) {...}

  auto loaded_process = fml::NativeLibrary::CreateForCurrentProcess();
  // 依据 kVMDataSymbol 从 native library 中加载
  return DartSnapshotBuffer::CreateWithSymbolInLibrary(loaded_process, DartSnapshot::kVMDataSymbol);
}

对于 iOS 来说,它默认会依据 kVMDataSymbol 来从 App 中加载对应资源,而其实 settings 是给提供了通过 path 的形式来加载资源和 snapshot 入口,那么对于 flutter_assets、icudtl.dat 这些动态资源,咱们齐全能够将其移出托管到服务端,而后动静下发。

而因为 iOS 零碎的限度,整个 App 可执行文件则不能够动静下发,但在第二局部的介绍中咱们理解到,其实 App 是由 kDartVmSnapshotData、kDartVmSnapshotInstructions、kDartIsolateSnapshotData、kDartIsolateSnapshotInstructions 等四个局部组成的,其中 kDartIsolateSnapshotInstructions、kDartVmSnapshotInstructions 为指令段,不可通过动静下发的形式来加载,而 kDartIsolateSnapshotData、kDartVmSnapshotData 为数据段,它们在加载时不存在限度。

到这里,其实咱们就能够失去 iOS 侧 Flutter 包大小的优化计划:将 flutter_assets、icudtl.dat 等动态资源及 kDartVmSnapshotData、kDartIsolateSnapshotData 两局部在编译时拆分进来,通过动静下发的形式来实现包大小的缩减。但此计划有个问题,kDartVmSnapshotData、kDartIsolateSnapshotData 是在编译时就写入到 App 中了,如何实现自动化地把此局部拆分进来是一个待解决的问题。为了解决此问题,咱们须要先理解 kDartVmSnapshotData、kDartIsolateSnapshotData 的写入机会。接下来,咱们通过下图 6 来简略地介绍一下该过程:

代码通过 gen_snapshot 工具来进行编译,它的入口在 gen_snapshot.cc 文件,通过初始化、预编译等过程,最终调用 Dart_CreateAppAOTSnapshotAsAssembly 办法来写入 snapshot。因而,咱们能够通过批改此流程,在写入 snapshot 时只将 instructions 写入,而将 data 重定向输出到文件,即可实现 kDartVmSnapshotData、kDartIsolateSnapshotData 与 App 的拆散。此局部流程示意图如下图 7 所示:

3.1.2 工程化计划

在实现了 App 数据段与代码段拆散的工作后,咱们就能够将数据段及资源文件通过动静下发、运行时加载的形式来实现包体积的缩减。由此思路衍生的 iOS 侧整体计划的架构如下图 8 所示;其中定制编译产物阶段次要负责定制 Flutter engine 及 Flutter SDK,以便实现产物的“瘦身”工作;公布集成阶段则为产物的公布和工程集成提供了一套标准化、自动化的解决方案;而运行阶段的使命是保障“瘦身”的资源在 engine 启动的时候能被平安稳固地加载。

注:图例中 MTFlutterRoute 为 Flutter 路由容器,MWS 指的是美团云。

3.1.2.1 定制编译产物阶段

尽管咱们不能把 App.framework 及 Flutter.framework 通过动静下发的形式齐全拆分进来,但能够剥离出局部非装置时必须的产物资源,通过动静下发的形式来达到 Flutter 包体积缩减的目标,因而在该阶段次要工作包含三局部。

1. 新增编译 command

在将 Flutter 包瘦身工程化时,咱们必须保障现有的流程的编译规定不会被影响,须要思考以下两点:

  • 减少编译“瘦身”的 Flutter 产物构建模式, 该模式应能编译出 AOT 模式下的瘦身产物。
  • 不对惯例的编译模式(debug、profile、release)引入影响。

对于 iOS 平台来说,AOT 模式 Flutter 产物编译的要害工作流程图如下图 9 所示。runCommand 会将编译所需参数及环境变量封装传递给编译后端(gen_snapshot 负责此局部工作),进而实现产物的编译工作:

为了实现“瘦身”的工作流,工具链在图 9 的流程中新增了 buildwithoutdata 的编译 command,该命令针对通过传递相应参数(without-data=true)给到编译后端(gen_snapshot),为后续编译出剥离 data 段提供撑持:

if [[$# == 0]]; then
  # Backwards-compatibility: if no args are provided, build.
  BuildApp
else
  case $1 in
    "build")
      BuildApp ;;
    "buildWithoutData")
      BuildAppWithoutData ;;
    "thin")
      ThinAppFrameworks ;;
    "embed")
      EmbedFlutterFrameworks ;;
  esac
fi

..addFlag('without-data',
        negatable: false,
        defaultsTo: false,
        hide: true,
  )

2. 编译后端定制

该局部次要对 gen_snapshot 工具进行定制,当 gen_snapshot 工具在接管到 Dart 层传来的“瘦身”命令时,会解析参数并执行咱们定制的办法 Dart_CreateAppAOTSnapshotAsAssembly,该局部次要做了两件事:

  1. 定制产物编译过程,生成剥离 data 段的编译产物。
  2. 重定向 data 段到文件中,以便后续进行应用。

具体到解决的细节,首先咱们须要在 gen_sanpshot 的入口解决传参,并指定重定向 data 文件的地址:

  CreateAndWritePrecompiledSnapshot() {
    ...
    if (snapshot_kind == kAppAOTAssembly) { // 惯例 release 模式下产物的编译流程
      ...
    } else if (snapshot_kind == kAppAOTAssemblyDropData) { 
      ...
      result = Dart_CreateAppAOTSnapshotAsAssembly(StreamingWriteCallback, 
                                                   file, 
                                                   &vm_snapshot_data_buffer,
                                                   &vm_snapshot_data_size,
                                                   &isolate_snapshot_data_buffer,
                                                   &isolate_snapshot_data_size,
                                                   true); // 定制产物编译过程,生成剥离 data 段的编译产物 snapshot_assembly.S
      ...
    } else if (...) {...}
    ...
  }

在承受到编译“瘦身”模式的命令后,将会调用定制的 FullSnapshotWriter 类来实现 Snapshot_assembly.S 的生成,该类会将原有编译过程中 vm_snapshot_data、isolate_snapshot_data 的写入过程改写成缓存到 buff 中,以便后续写入到独立的文件中:

// drop_data=true, 示意后瘦身模式的编译过程
// vm_snapshot_data_buffer、isolate_snapshot_data_buffer 用于保留 vm_snapshot_data、isolate_snapshot_data 以便后续写入文件
Dart_CreateAppAOTSnapshotAsAssembly(Dart_StreamingWriteCallback callback,
                                    void* callback_data, 
                                    bool drop_data,
                                    uint8_t** vm_snapshot_data_buffer,
                                    uint8_t** isolate_snapshot_data_buffer) {
  ...
  FullSnapshotWriter writer(Snapshot::kFullAOT, &vm_snapshot_data_buffer,
                            &isolate_snapshot_data_buffer, ApiReallocate,
                            &image_writer, &image_writer);

  if (drop_data) {writer.WriteFullSnapshotWithoutData(); // 拆散出数据段
  } else {writer.WriteFullSnapshot();
  }
  ...
}

当 data 段被缓存到 buffer 中后,便能够应用 gen_snapshot 提供的文件写入的办法 WriteFile 来实现数据段以文件模式从编译产物中拆散:

static void WriteFile(const char* filename, const uint8_t* buffer, const intptr_t size);
// 写 data 到指定文件中
{
  ...
      WriteFile(vm_snapshot_data_filename, vm_snapshot_data_buffer, vm_snapshot_data_size); // 写入 vm_snapshot_data
      WriteFile(isolate_snapshot_data_filename, isolate_snapshot_data_buffer, isolate_snapshot_data_size); // 写入 isolate_snapshot_data
  ...
}

3.engine 定制

编译参数批改

iOS 侧应用 -0z 参数能够取得包体积缩减的收益(大概为 700KB 左右的收益),但会有相应的性能损耗,因而该局部作为一个可选项提供给业务方,工具链提供相应版本的 Flutter engine 的定制。

资源加载形式定制

对于 engine 的定制,次要围绕如何“手动”引入拆分出的资源来开展,好在 engine 提供了 settings 接口让咱们能够实现自定义引入文件的 path,因而咱们须要做的就是对 Flutter engine 初始化的过程进行相应革新:

/**
 * custom icudtl.dat path
 */
@property(nonatomic, copy) NSString* icuDataPath;
​
/**
 * custom flutter_assets path
 */
@property(nonatomic, copy) NSString* assetPath;
​
/**
 * custom isolate_snapshot_data path
 */
@property(nonatomic, copy) NSString* isolateSnapshotDataPath;
​
/**
 *custom vm_snapshot_data path
 */
@property(nonatomic, copy) NSString* vmSnapshotDataPath;

在运行时“手动”配置上述门路,并联合上述参数初始化 FlutterDartProject,从而达到 engine 启动时从配置门路加载相应资源的目标。

engine 编译自动化

在实现 engine 的定制和革新后,还须要手动编译一下 engine 源码,生成各平台、架构、模式下的产物,并将其集成到 Flutter SDK 中,为了让引擎定制的流程标准化、自动化,MTFlutter 工具链提供了一套 engine 自动化编译公布的工具。如流程图 10 所示,在实现 engine 代码的自定义批改之后,工具链会依据 engine 的 patch code 编译出各平台、架构及不同模式下的 engine 产物,而后主动上传到美团云上,在开发和打包时只须要通简略的命令,即可装置和应用定制后的 Flutter engine:

3.1.2.2 公布集成阶段

当实现 Dart 代码编译产物的定制后,咱们下一步要做的就是革新 MTFlutter 工具链现有的产物公布流程,反对打出“瘦身”模式的产物,并将瘦身模式下的产物进行正当的组织、封装、托管以不便产物的集成。从工具链的视角来看,该局部的流程示如下图 11 所示:

自动化公布与版本治理

MTFlutter 工具链将“瘦身”集成到产物公布的流水线中,新增一种 thin 模式下的产物,在 iOS 侧该产物包含 release 模式下瘦身后的 App.framework、Flutter.framework 以及拆分出的数据、资源等文件。当开发者提交了代码并应用 Talos(美团外部前端继续交付平台)触发 Flutter 打包时,CI 工具会主动打出瘦身的产物包及须要运行时下载的资源包、生成产物相干信息的校验文件并主动上传到美团云上。对于产物资源的版本治理,咱们则复用了美团云提供资源管理的能力。在美团云上,产物资源以文件目录的模式来实现各版本资源的互相隔离,同时对“瘦身”资源独自开一个 bucket 进行独自治理,在集成产物时,集成插件只需依据以后产物 module 的名称及版本号便可获取对应的产物。

自动化集成

针对瘦身模式 MTFlutter 工具链对集成插件也进行了相应的革新,如下图 12 所示。咱们对 Flutter 集成插件进行了批改,在原有的产物集成模式的根底上新增一种 thin 模式,该模式在表现形式与原有的 debug、release、profile 相似,区别在于:为了不便开发人员调试,该模式会根据以后工程的 buildconfigration 来做相应的解决,即在 debug 模式下集成原有的 debug 产物,而在 release 模式下才集成“瘦身”产物包。

3.1.2.3 运行阶段

运行阶段所解决的外围问题包含资源下载、缓存、解压、加载及异样监控等。一个典型的瘦身模式下的 engine 启动的过程如图 13 所示。

该过程包含:

  • 资源下载 :读取工程配置文件,失去以后 Flutter module 的版本,并查问和下载近程资源。
  • 资源解压和校验 :对下载资源进行完整性校验,校验实现则进行解压和本地缓存。
  • 启动 engine:在 engine 启动时加载下载的资源。
  • 监控和异样解决 :对整个流程可能呈现的异常情况进行解决,相干数据状况进行监控上报。

为了不便业务方的应用、缩小其接入老本,MTFlutter 将该局部工作集成至 MTFlutterRoute 中,业务方仅需引入 MTFlutterRoute 即可将“瘦身”性能接入到我的项目中。

3.2 Android 侧计划

3.2.1 整体架构

在 Android 侧,咱们做到了除 Java 代码外的所有 Flutter 产物都动静下发。残缺的优化计划概括来说就是:动静下发 + 自定义引擎初始化 + 自定义资源加载。计划整体分为打包阶段和运行阶段,打包阶段会将 Flutter 产物移除并生成瘦身的 APK,运行阶段则实现产物下载、自定义引擎初始化及资源加载。其中产物的上传和下载由 DynLoader 实现,这是由美团平台迭代工程组提供的一套 so 与 assets 的动静下发框架,它包含编译时和运行时两局部的操作:

  1. 工程配置:配置须要上传的 so 和 assets 文件。
  2. App 打包时,会将配置 1 中的文件压缩上传到动静公布零碎,并从 APK 中移除。
  3. App 每次启动时,向动静公布零碎发动申请,申请须要下载的压缩包,而后下载到本地并解压,如果本地曾经存在了,则不进行下载。

咱们在 DynLoader 的根底上,通过对 Flutter 引擎初始化及资源加载流程进行定制,设计了整体的 Flutter 包大小优化计划:

打包阶段 :咱们在原有的 APK 打包流程中,退出一些自定义的 gradle plugin 来对 Flutter 产物进行解决。在预处理流程,咱们将一些无用的资源文件移除,而后将 flutter_assets 中的文件打包为 bundle.zip。而后通过 DynLoader 提供的上传插件将 libflutter.so、libapp.so 和 flutter_assets/bundle.zip 从 APK 中移除,并上传到动静公布零碎托管。其中对于多架构的 so,咱们通过在 build.gradle 中减少 abiFilters 进行过滤,只保留单架构的 so。最终打包进去的 APK 即为瘦身后的 APK。

不经解决的话,瘦身后的 APK 一进到 Flutter 页面必定会报错,因为此时 so 和 flutter_assets 可能都还没下载下来,即便曾经下载下来,其地位也产生了扭转,再应用原来的加载形式必定会找不到。所以咱们在运行阶段须要做一些非凡解决:

1.Flutter 路由拦挡

首先要应用 Flutter 路由拦截器,在进到 Flutter 页面之前,要确保 so 和 flutter_assets 都曾经下载实现,如果没有下载完,则显示 loading 弹窗,而后调用 DynLoader 的办法去异步下载。当下载实现后,再执行原来的跳转逻辑。

2. 自定义引擎初始化

第一次进到 Flutter 页面,须要先初始化 Flutter 引擎,其中次要是将 libflutter.so 和 libapp.so 的门路改为动静下发的门路。另外还须要将 flutter_assets/bundle.zip 进行解压。

3. 自定义资源加载

当引擎初始化实现后,开始执行 Dart 代码的逻辑。此时必定会遇到资源加载,比方字体或者图片。原有的资源加载器是通过 method channel 调用 AssetManager 的办法,从 APK 中的 assets 中进行加载,咱们须要改成从动静下发的门路中加载。

上面咱们具体介绍下某些局部的具体实现。

3.2.2 自定义引擎初始化

原有的 Flutter 引擎初始化由 FlutterMain 类的两个办法实现,别离为 startInitialization 和 ensureInitializationComplete,个别在 Application 初始化时调用 startInitialization(懒加载模式会提早到启动 Flutter 页面时再调用),而后在 Flutter 页面启动时调用 ensureInitializationComplete 确保初始化的实现。

在 startInitialization 办法中,会加载 libflutter.so,在 ensureInitializationComplete 中会构建 shellArgs 参数,而后将 shellArgs 传给 FlutterJNI.nativeInit 办法,由 jni 侧实现引擎的初始化。其中 shellArgs 中有个参数 AOT_SHARED_LIBRARY_NAME 能够用来指定 libapp.so 的门路。

自定义引擎初始化,次要要批改两个中央,一个是 System.loadLibrary(“flutter”),一个是 shellArgs 中 libapp.so 的门路。有两种方法能够做到:

  1. 间接批改 FlutterMain 的源码,这种形式简略间接,然而须要批改引擎并从新打包,业务方也须要应用定制的引擎才能够。
  2. 继承 FlutterMain 类,重写 startInitialization 和 ensureInitializationComplete 的逻辑,让业务方应用咱们的自定义类来初始化引擎。当自定义类实现引擎的初始化后,通过反射的形式批改 sSettings 和 sInitialized,从而使得原有的初始化逻辑不再执行。

本文应用第二种形式,须要在 FlutterActivity 的 onCreate 办法中首先调用自定义的引擎初始化办法,而后再调用 super 的 onCreate 办法。

3.2.3 自定义资源加载

Flutter 中的资源加载由一组类实现,依据数据源的不同分为了网络资源加载和本地资源加载,其类图如下:

AssetBundle 为资源加载的抽象类,网络资源由 NetworkAssetBundle 加载,打包到 Apk 中的资源由 PlatformAssetBundle 加载。

PlatformAssetBundle 通过 channel 调用,最终由 AssetManager 去实现资源的加载并返回给 Dart 层。

咱们无奈批改 PlatformAssetBundle 原有的资源加载逻辑,然而咱们能够自定义一个资源加载器对其进行替换:在 widget 树的顶层通过 DefaultAssetBundle 注入。

自定义的资源加载器 DynamicPlatformAssetBundle,通过 channel 调用,最终从动静下发的 flutter_assets 中加载资源。

3.2.4 字体动静加载

字体属于一种非凡的资源,其有两种加载形式:

  1. 动态加载 :在 pubspec.yaml 文件中申明的字体及为动态加载,当引擎初始化的时候,会主动从 AssetManager 中加载动态注册的字体资源。
  2. 动静加载 :Flutter 提供了 FontLoader 类来实现字体的动静加载。

当资源动静下发后,assets 中曾经没有字体文件了,所以动态加载会失败,咱们须要改为动静加载。

3.2.5 运行时代码组织构造

整个计划的运行时局部波及多个功能模块,包含产物下载、引擎初始化、资源加载和字体加载,既有 Native 侧的逻辑,也有 Dart 侧的逻辑。如何将这些模块正当的加以整合呢?平台团队的同学给了很好的答案,并将其实现为一个 Flutter Plugin:flutter_dynamic(美团外部库)。其整体分为 Dart 侧和 Android 侧两局部,Dart 侧提供字体和资源加载办法,办法外部通过 method channel 调到 Android 侧,在 Android 侧基于 DynLoader 提供的接口实现产物下载和资源加载的逻辑。

四、计划的接入与应用

为了让大家理解上述计划应用层面的设计,咱们在此把美团外部的应用形式介绍给大家,其中会波及到一些外部工具细节咱们暂不开展,重点解释设计和应用体验局部。因为 Android 和 iOS 的实现计划有所区别,故在接入形式相应的也会有些差别,上面针对不同平台离开来介绍:

4.1 iOS

在上文计划的设计中,咱们介绍到包瘦身性能曾经集成进入美团外部 MTFlutter 工具链中,因而当业务方在应用了 MTFlutter 后只需简略的几步配置便可实现包瘦身性能的接入。iOS 的接入应用上总体分为三步:

1. 引入 Flutter 集成插件(cocoapods-flutter-plugin 美团外部 Cocoapods 插件,进一步封装 Flutter 模块引入,使之更加清晰便捷):

gem 'cocoapods-flutter-plugin', '~> 1.2.0'

2. 接入 MTFlutterRoute 混合业务容器(美团外部 pod 库,封装了 Flutter 初始化及全局路由等能力),实现基于“瘦身”产物的初始化:

Flutter 业务工程中引入 mt_flutter_route:

dependencies:
  mt_flutter_route: ^2.4.0

3. 在 iOS Native 工程中引入 MTFlutterRoute pod:

binary_pod 'MTFlutterRoute', '2.4.1.8'

通过下面的配置后,失常 Flutter 业务发版时就会主动产生“瘦身”后的产物,此时只需在工程中配置瘦身模式即可实现接入:

flutter 'your_flutter_project', 'x.x.x', :thin => true

4.2 Android

4.2.1 Flutter 侧批改

  1. 在 Flutter 工程 pubspec.yaml 中增加 flutter_dynamic(美团外部 Flutter Plugin,负责 Dart 侧的字体、资源加载)依赖。
  2. 在 main.dart 中增加字体动静加载逻辑,并替换默认资源加载器。
void main() async {
   // 动静加载字体
  await dynFontInit();
  // 自定义资源加载器
  runApp(DefaultAssetBundle(
    bundle: dynRootBundle,
    child: MyApp(),));
}

4.2.2 Native 侧批改

1. 打包脚本批改

在 App 模块的 build.gradle 中通过 apply 特定 plugin 实现产物的删减、压缩以及上传。

2. 在 Application 的 onCreate 办法中初始化 FlutterDynamic。

3. 增加 Flutter 页面跳转拦挡。

在跳转到 Flutter 页面之前,须要应用 FlutterDynamic 提供的接口来确保产物曾经下载实现,在下载胜利的回调中来执行真正的跳转逻辑。

class FlutterRouteUtil {public static void startFlutterActivity(final Context context, Intent intent) {FlutterDynamic.getInstance().ensureLoaded(context, new LoadCallback() {
            @Override
            public void onSuccess() {
              // 在下载胜利的回调中执行跳转逻辑
                context.startActivity(intent);
            }
        });
    }
}

备注:如果 App 有应用相似 WMRoute 之类的路由组件的话,能够自定义一个 UriHandler 来对立解决所有的 Flutter 页面跳转,同样在 ensureLoaded 办法回调中执行真正的跳转逻辑。

4. 增加引擎初始化逻辑

咱们须要重写 FlutterActivity 的 onCreate 办法,在 super.onCreate 之前先执行自定义的引擎初始化逻辑。

public class MainFlutterActivity extends FlutterActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) 
      // 确保自定义引擎初始化实现
        FlutterDynamic.getInstance().ensureFlutterInit(this);
        super.onCreate(savedInstanceState);
    }
}

五、总结瞻望

目前,动静下发的计划已在美团外部 App 上线应用,Android 包瘦身成果达到 95%,iOS 包瘦身成果达到 30%+。动静下发的计划尽管能显著缩小 Flutter 的包体积,但其收益是通过运行时下载的形式置换回来的。当 Flutter 业务的一直迭代增长时,Flutter 产物包也会随之一直变大,最终导致需下载的产物变大,也会对下载成功率带来压力。后续,咱们还会摸索 Flutter 的分包逻辑,通过将不同的业务模块拆分来升高单个产物包的大小,来进一步保障包瘦身性能的可用性。

六、作者简介

  • 艳东,2018 年退出美团,到家平台前端工程师。
  • 宗文,2019 年退出美团,到家平台前端高级工程师。
  • 会超,2014 年退出美团,到家平台前端技术专家。

招聘信息

美团外卖长期招聘 Android、iOS、FE 高级 / 资深工程师和技术专家。欢送感兴趣的同学投递简历至:tech@meituan.com(邮件题目请注明:美团外卖技术团队)。

浏览更多技术文章,请扫码关注微信公众号 - 美团技术团队!

正文完
 0