阅读 1661

【译】Flutter 延迟组件原理与自定义 【包体积优化 | 动态化】

原文:Deferred-Components

作者:Google 工程师 Gary Qian

上一篇文章中,我们从操作层面介绍了延迟组价是什么?已经该如何应用?本篇文章将会和大家一起探索延迟组件的更多技术细节。以及该如何自定义实现延迟加载功能。

简介

Flutter 支持构建在运行时下载额外 Dart 代码和静态资源的应用程序。这可以减少安装应用程序 apk 的大小,并在用户需要时下载功能和静态资源。

我们将每个独立的可下载的 Dart 库和静态资源称为「延迟组件」。此功能目前仅在 Android 可用,延迟组件中的代码不会影响其他平台,其他平台在初始安装时会正常构建包含所有延迟组件和资源的应用。

延迟加载仅在应用程序编译为 Release 或 Profile 模式 时可用。在 Debug 模式下,所有延迟组件都被视为常规导入,它们在启动时立即加载。因此,Debug 模式下仍然可以热重载。

Gallery 案例

Flutter Gallery 在 fully deferred Flutter gallery branch 分支,将其中所有的 demo 改为了延迟加载组件,对比未使用延迟加载的情况下,安装 Apk 文件的大小数据如下:

使用延迟组件:

  • base-arm64_v8a.apk - 12,325,372 bytes
  • base-master.apk - 37,889,309 bytes
  • 安装包大小: 50,214,681 bytes

未使用延迟组件:

  • base-arm64_v8a.apk - 12,521,900 bytes
  • base-master.apk - 80,605,796 bytes
  • 安装包大小: 93,127,696 bytes

我们可以看到编译后的代码大小(base-arm64_v8a.apk) 减少了约 200kB,初始包体积(base-master.apk)减少了约 43MB。总体而言,初始安装尺寸减少了46%。dart代码、资源文件等会被移动到单独的组件中,只有在需要时才会在运行时下载。安装了所有组件后应用的体积只比非延迟安装的应用仅多几 KB。

延迟组件的应用结构

延期的 Dart 库通过 gen_snapshot(Dart编译器)生成「加载单元」,当以 profile 或者 release 模式构建的时候,每个加载单元输出为一个拆分的 AOT 共享库(.so 文件)。加载单元是代码中用 deferred 关键字引入库的最小的集合,可以从基础库中分离出来。

下图展示了使用延迟组件的应用程序结构,和延迟 dart 库被编译成加载单元并打包成 .aab 文件的「生命周期」。

这个例子有以下特点:

  • 四个 Dart 库,其中 Dart 库 lib1 依赖于 lib2。lib1、lib3 和 lib4 被作为延迟组件导入到 flutter 应用程序的主代码中。
  • 四个加载单元,其中 id 为 1 的是基本单元,加载单元 2 同时包含 lib1 和 lib2。加载单元 3 和 4 分别包含 lib3 和 lib4。
  • 三个定义的延迟组件,加上一个隐式基本组件。延迟组件 1 包含加载单元 2 和静态资源。延迟组件 2 包含加载单元 3 和 4,没有静态资源。而延迟组件 3 是一个仅含静态资源的组件。
  • app-release.aab 是完整的构建输出文件,包含三个延迟组件以及基本组件。

.aab 文件中总包含一个未显式声明的基本组件,它包含核心的 Flutter 包以及最基本的应用程序代码。任何未被延迟加载的库都会被包含在基本加载单元中。如果没有生成基本单元以外的加载单元,这可能意味着延迟导入的文件不正确。

对于延迟导入的库而言,里面非延迟导入的文件,会被编译到一个加载单元中:

loadLibrary() 调用的生命周期

延迟组件主要通过 dart 中的 loadLibrary() 调用来触发被下载、安装和加载。这个调用在 dart2js 和 aot/native 中的处理是不同的。这里,我们梳理 loadLibrary() 调用转换为一个延迟组件的安装过程:

dart 中 loadLibrary() 会调用的 native 层的 Dart_DeferredLoadHandler 函数,该回调函数在DartIsolate::Initialize 中由 Dart_SetDeferredLoadHandler 设置。Dart 在内部检索分配给库的加载单元 ID,并将其传递给回调函数。回调到 DartIsolate::OnDartLoadLibrary。

加载单元 ID 然后通过 runtime controller、engine 和 platform view 传递到 Android 嵌入层中的 FlutterJNI。这里,加载单元 ID 被传递到 DeferredComponentsManagerinstallDeferredComponent 方法中, ID 从一个整数映射为一个 String 名称,标识请求库所属的 pubspec 定义的延迟组件。这个转换由 AndroidManifest 中的 meta-data 映射,在构建阶段创建并验证。

之后 PlayStoreDeferredComponentManager 调用 API 下载 module。module 安装会定位到 .so 文件,并将路径传递给 engine 执行 dloopen。engine 将解析的符号发送到 dart isolate,以此将这些符号加载到 dart VM 中。整个加载过程必须与加载单元 ID 关联,否则 loadLibrary() 返回的 Future 对象不会完成。

请记住,多个加载单元可能包含在一个延迟组件中,但是 loadLibrary 只会从调用的特定 dart 库中加载 dart 符号。每个加载单元在使用前必须单独的调用 loadLibrary。对已经下载过的组件,后续调用 loadLibrary 不会二次加载,但是也并非同步完成,在调用和完成之间至少有一帧间隔。

通过延迟组件名称进行安装

我们还提供了 framework-side DeferredComponent utility class,它允许通过延迟组件名来直接安装。

这个方法可以有两个用途:

  • 安装只有静态资源的延迟组件。
  • 提前下载延迟组件以供以后使用。但是,为了使用提前下载组件中的 dart 代码,仍然必须调用loadLibrary() 。

这个直接的 API 通过 platform channels 直接调用 DynamicFeatureManagerinstallDeferredComponent 方法,并且由于未指定的加载单元,不会加载组件的任何 dart 代码。仅有静态资源被加载。要使用 dart 代码,还必须调用 loadLibrary()

卸载

DeferredComponent 的工具类中还提供了 uninstallDeferredComponent 方法,该方法使用 platform channels 请求操作系统卸载并删除与指定的延迟组件关联的文件。不同平台的卸载行为也不一样,在 Android 中文件的删除是排队的,在实际执行之前可能需要很长时间。

只能用将要卸载的组件名称来请求卸载。目前还不支持通过加载单元 id 或直接调用 dart 来卸载。

工具

延迟组件必须构建为 Android App Bundles (.aab) 才能正常工作。如果构建为调试文件或 apk 文件,则 dart 将正常编译并生成一个 .so 文件。

延迟组件使用 $ flutter build appbundle 命令构建,它会检查 pubspec.yaml 中是否存在 deferred-components: 来决定是否延迟构建。当应用程序中包含延迟组件并且构建模式为 profile 或者release,gen_snapshot 会收到一个 ——loading_unit_manifest 路径,它告诉 gen_snapshot 生成拆分的 AOT 产物,包含一个基本文件,以及一个 .so 用于代码库中的每个延迟库。这些分割的单元称为「加载单元」,并被分配一个内部整数 ID,称为加载单元 ID。

构建过程还依赖于项目设置来发挥作用。每个延迟组件必须对应于应用程序的 android 目录下定义的 android module。基本 module 被构建为 app ,而每个附加组件应该有一个与该组件同名的 module。基本模块AndroidManifest.xml 也需要包含加载单元 id 和延迟组件之间的映射。

flutter build appbundle 命令会执行一个验证程序,这个验证程序会指导开发人员完成正确的构建。验证程序是必要的,因为在 gen_snapshot 完成编译之前,无法知道 gen_snapshot 生成的加载单元。因此,某些项目设置只能 gen_snapshot 步骤之后才能完成。

因为错误地将延迟组件库作为非延迟组件库导入会导致文件被编译到基本加载单元中,所以延迟组件验证程序也有一种机制来防止对应用最终生成的加载单元的意外更改。如果生成的加载单元与缓存在 deferred_components_loading_units.yaml 文件中前一次运行的结果不匹配,则此检查将导致构建失败。在检测到由更改而抛出错误后,如果没有进行其他更改,构建将在下次运行时自动通过此检查。这意味着这个检查不是错误证明,因为你仍然可以忽略不匹配的加载单元错误,并继续构建。

自定义实现

可以不通过 Android Play 商店实现自定义下载。这只推荐给高级开发者,主要针对具有特殊需求的应用,如超大的静态资源,某些特定地下载行为,或无法访问 Play 商店的地区(如中国)。

简介

Flutter 嵌入层允许自定义实现,处理自定义的的延迟组件下载和解压,同时仍然允许访问核心的 Dart 回调,该回调将加载单元注册到 Dart runtime。这个过程比默认的 play store 版本要复杂。

要实现一个自定义延迟组件系统,主要包含以下部分:

  • DeferredComponentManagerAndroid 嵌入层的实现,用于处理应用程序和服务器之间的通信,并从下载的组件中提取 .so 文件和静态资源。

  • DeferredComponentManager 兼容的打包组件的工具,并解释加载单元的 gen_snapshot 输出。

  • 存放组件的服务器,如果没有 Play 商店作为动态功能模块下发,这就必须定制的。

下面的部分会做详细的指导:

DeferredComponentManager- 安卓嵌入层

嵌入层负责下载和安装打包的组件文件。这可以通过在 Android 嵌入层中继承抽象类 DeferredComponentManager 来实现

installDeferredComponent 是这个类的入口 ,它提供了加载单元 id 和组件名称,来确定要安装什么组件。loadLibrary() 的调用传递唯一的一个加载单元 id,而框架层中的DeferredComponent.installDeferredComponent() 的调用需要传入位移的组件名来加载只含静态资源的组件。

为了将加载单元 id 解析为特定的组件,通常需要存储加载单元 id 到组件名称的映射。在默认实现中,我们通过在应用程序中的 AndroidManifest.xml 中存储一个键值对数据来实现,但这可以以任何想要的方式实现。

你可以在 engine 源码中的 shell/platform/android/io/flutter/embedded /engine/deferredcomponents/DeferredComponentManager.java 中找到 DeferredComponentManager 每个方法的详细解释。默认的 Play store 实现可以在 shell/platform/android/io/flutter/ embedded /engine/deferredcomponents/PlayStoreDeferredComponentManager.java 中找到,可以作为一个粗略的实现指南。

要加载 Dart 库,请提供加载单元 id 和路径列表并调用 FlutterJNI.loadartdeferredlibrary 提供,这些路径包含你的 loadartlibrary 实现中的 .so 文件。engine 会尝试检索提供的每一个路径,直到文件被成功打开。

要加载新静态资源,创建一个可以访问新下载的静态资源的资源管理器。通过 FlutterJNI.updateJavaAssetManager 更新。

FlutterJNI 实例是通过 setJNI 传入。

工具

Flutter 的构建工具有引导 gen_snapshot 构建为拆分的 AOT 以及打包 .so 文件和静态资源到 Android 动态 module 的能力。自定义实现通常无法使用此工具。因此,您可能必须编写自定义工具来打包 .so 文件和静态资源,以便与自定义的 DeferredComponentManager 协同工作。

要让 gen_snapshot 生成加载单元和 .so 共享库,请将 ——loading_unit_manifest=<manifestPath> 配置传递给 gen_snapshot。他会在你的 manifestPath 中创建一个 .json 文件,包含加载单元和相应生成的 .so 库。之后,可以将 so 文件和静态打包成您希望在文件服务器上发布的任何格式。你还需要在 DeferredComponentManager 实现类中解析文件。

文件服务器

由于自定义实现通常不使用 Play 商店,用户应该实现一个文件管理服务。这部分的实现方式是很灵活的,唯一的要求是它能与 DeferredComponentManager 实现协同工作,以传输加载 Dart 共享库和静态资源所需的文件。

最后

其实根据 Gallery 的案例,我们可以看出,如果安装包中静态资源占比很高时,延迟加载对于 app 体积优化非常明显。而国内必须要通过自定义的方式实现延迟组件的管理,这本身是有成本的。所以,这项技术是否真正适合落地于业务中还需要评估收益比。但从技术本身的角度来看,里面可玩性还挺高的,提供动态加载库的能力其实可以发散很多方向。后面有空也尝试实践落地,如果你也感兴趣欢迎关注,点赞,在评论区留下你的看法。

列表流畅度优化分帧组件即将走完审核流程,预计两周内完成发布,欢迎关注我的动态。

往期优质专栏:

如何设计并实现一个高性能的Flutter列表

Flutter核心渲染机制

Flutter路由设计与源码解析

Flutter事件分发

公众号:进击的Flutter或者 runflutter 里面整理收集了最详细的Flutter进阶与优化指南,欢迎关注。

文章分类
Android
文章标签