阅读 3697

【译】 Flutter 延迟加载组件 【包体积优化 | 动态化】

Flutter 2.2 带来了很多新的功能,其中最让我感兴趣的便是「Deferred Components」延迟加载组件。这一特性可以让我们将 Flutter 产物拆分为多个组件,并在需要时再进行下载。 借助于这一特性,官方 Gallery 演示程序安装时的大小压缩了 46% (200KB 的代码和 43MB 的资源减少)本文向大家介绍,如何使用延迟加载功能

文章已发布于 Flutter 中文文档 -> 性能优化 -> 延迟加载组件

本文原作:Google 工程师 Gary Qian

简介

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

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

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

关于此功能的技术细节,请查看 Flutter wiki 上的 Flutter 延迟组件原理与自定义


如何让项目支持延迟加载组件

下面的引导将介绍如何设置 Android 应用程序以支持延迟加载。

译者:注意国内无法使用 Google Play 做产物下发,需实现 DeferredComponentManager 自定义下载模块。

步骤 1:依赖项和初始项目设置

  1. 将 Play Core 添加到 Android 应用程序的 build.gradle 依赖项中。 在 `android/app/build.gradle` 中添加以下内容:

    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    复制代码
  2. 如果使用 Google Play 商店作为动态功能的分发模型, 应用程序必须支持 `SplitCompat` 并手动提供 `PlayStoreDeferredComponentManager` 的实例。 这两个任务都可以通过设置 `android/app/src/main/AndroidManifest.xml` 中的 `android:name` 为 `io.flatter.app.flatterPlayStoreSplitApplication` 应用属性来完成:

    <manifest ...
      <application
         android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
            ...
      </application>
    </manifest>
    复制代码

    io.flutter.app.FlutterPlayStoreSplitApplication 已经为你完成了这两项任务。 如果你使用了 FlutterPlayStoreSplitApplication,可以跳过步骤 1.3。

    如果你的 Android 应用程序很大或很复杂, 你可能需要单独支持 SplitCompat 并提供 PlayStoreDynamicFeatureManager

    要支持 SplitCompat,有三种方法(详见 Android docs),其中任何一种都是有效的:

    • 让你的 application 类继承 `SplitCompatApplication`:

      public class MyApplication extends SplitCompatApplication {
          ...
      }
      复制代码
    • 在 `attachBaseContext()` 中调用 `SplitCompat.install(this);`:

      @Override
      protected void attachBaseContext(Context base) {
          super.attachBaseContext(base);
          // Emulates installation of future on demand modules using SplitCompat.
          SplitCompat.install(this);
      }
      复制代码
    • 将 `SplitCompatApplication` 声明为 application 的子类, 并将 `FlutterApplication` 中的 flutter 兼容性代码添加到你的 application 类中:

      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      复制代码

    嵌入层依赖注入的 DeferredComponentManager 实例来处理延迟组件的安装请求。 通过在应用程序的初始流程中添加以下代码,将 PlayStoreDeferredComponentManager 添加到 Flutter 嵌入层中:

    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    import io.flutter.FlutterInjector;
    ... 
    layStoreDeferredComponentManager deferredComponentManager = new
      PlayStoreDeferredComponentManager(this, null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    复制代码
  3. 通过将 `deferred-components` 依赖添加到应用程序的 `pubspec.yaml` 中的 `flutter` 下,并选择延迟组件:

    ...
    flutter:
      ...
      deferred-components:
      ...
    复制代码

    flutter 工具会在 pubspec.yaml 中查找 deferred-components, 来确定是否应将应用程序构建为延迟加载。 除非你已经知道所需的组件和每个组件中的 Dart 延迟库,否则可以暂时将其留空。 当 gen_snapshot 生成加载单元后,你可以在后面的 步骤 3.3 中完善这部分内容。

步骤 2:实现延迟加载的 Dart 库

接下来,在 Dart 代码中实现延迟加载的 Dart 库。实现并非立刻需要的功能。 文章剩余部分中的示例添加了一个简单的延迟 widget 作为占位。 你还可以通过修改 loadLibrary()Futures 后面的延迟加载代码的导入和保护用法,将现有代码转换为延迟代码。

  1. 创建新的 Dart 库。例如,创建一个可以在运行时下载的 `DeferredBox` widget。 这个 widget 可以是任意复杂的,本指南使用以下内容创建了一个简单的框。

    // box.dart
    
    import 'package:flutter/widgets.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      DeferredBox() {}
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 30,
          width: 30,
          color: Colors.blue,
        );
      }
    }
    复制代码
  2. 在应用中使用 `deferred` 关键字导入新的 Dart 库,并调用 `loadLibrary()`。 下面的示例使用 `FutureBuilder` 等待 `loadLibrary` 的 `Future` 对象(在 `initState` 中创建)完成, 并将 `CircularProgressIndicator` 做为占位。 当 `Future` 完成时,会返回 `DeferredBox`。 `SomeWidget` 便可在应用程序中正常使用,在成功加载之前不会尝试访问延迟的 Dart 代码。

    import 'box.dart' deferred as box;
    
    // ...
    
    class SomeWidget extends StatefulWidget {
      @override
      _SomeWidgetState createState() => _SomeWidgetState();
    }
    
    class _SomeWidgetState extends State<SomeWidget> {
      Future<void> _libraryFuture;
    
      @override
      void initState() {
        _libraryFuture = box.loadLibrary();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<void>(
          future: _libraryFuture,
          builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              }
              return box.DeferredBox();
            }
            return CircularProgressIndicator();
          },
        );
      }
    }
    // ...
    复制代码

    loadLibrary() 函数返回一个 Future<void> 对象, 该对象会在延迟库中的代码可用时成功返回,否则返回一个错误。 延迟库中所有的符号在使用之前都应确保 loadLibrary() 已经完成。 所有导入的库都必须通过 deferred 标记,以便对其进行适当的编译以及在延迟组件中使用。 如果组件已经被加载,再次调用 loadLibrary 将快速返回(但不是同步完成)。 也可以提前调用 loadLibrary() 函数进行预加载,以帮助屏蔽加载时间。

    你可以在 Flutter Gallery’s lib/deferred_widget.dart 中找到其他延迟加载组件的示例。

步骤 3:构建应用程序

使用以下 flutter 命令构建延迟组件应用:

$ flutter build appbundle
复制代码

此命令会帮助你检查项目是否正确设置为构建延迟组件应用。 默认情况下,验证程序检测到任何问题都会导致构建失败,你可以通过系统建议的更改来修复这些问题。

你可以使用 --no-deferred-components 标志禁用构建延迟组件。 这个标志会让 pubspec.yaml 中定义的所有延迟组件,被视为定义在 assets 部分的普通组件。 所有 Dart 代码会被编译到一个共享库中,loadLibrary() 调用会在下一个事件循环中完成(异步时尽快完成)。 此标志也等效于移除 pubspec.yaml 中的 deferred-components:

  1. `flutter build appbundle` 命令会尝试构建应用, 通过 `gen_snapshot` 将应用中拆分的 AOT 共享库分割为单独的 `.so` 文件。 第一次运行时,验证程序可能会在检测到问题时失败, 该工具会为如何设置项目和解决这些问题提供建议。

    验证程序分为两个部分:预构建和生成快照后的验证。 这是因为在 gen_snapshot 完成并生成最后一组加载单元之前,无法执行任何引用加载单元的验证。

    你可以通过 --no-validate-deferred-components 标志,来让工具尝试在不执行验证程序下构建应用。 这可能导致由意外和错误的指令而引起的故障。 此标志应当仅在不需要依赖验证程序检查的默认 Play-store-based 的自定义实现时使用。

    验证程序会检测 gen_snapshot 生成的所有新增、修改或者删除的加载单元。 当前生成的加载单元记录在 <projectDirectory>/deferred_components_loading_units.yaml 文件中。 这个文件应该加入到版本管理中,以确保其他开发人员对加载单元所做的更改可被追踪。

    验证程序还会检查 android 目录中的以下内容:

    • 每个延迟组件名称的键值对映射 `${componentName}Name`:`${componentName}`。 每个功能模块的 `AndroidManifest.xml` 使用此字符串资源来定义 `dist:title property`。例如:

      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      复制代码
    • 每个延迟组件都有一个 Android 动态功能模块,它包含一个 `build.gradle` 和 `src/main/AndroidManifest.xml` 文件。 验证程序只检查文件是否存在,不验证文件内容。如果文件不存在,它将生成一个默认的推荐文件。

    • 包含一个 meta-data 键值对,对加载单元与其关联的组件名称之间的映射进行编码。 嵌入程序使用此映射将 Dart 的内部加载单元 id 转换为要安装的延迟组件的名称。例如:

          ...
          <application
              android:label="MyApp"
              android:name="io.flutter.app.FlutterPlayStoreSplitApplication"
              android:icon="@mipmap/ic_launcher">
              ...
              <meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="2:boxComponent"/>
          </application>
          ...
      复制代码

    gen_snapshot 验证程序在预构建验证通过之前不会运行。

  2. 对于每个检查,该工具会创建或者修改需要的文件。 这些文件放在 `/build/android_deferred_components_setup_files` 目录下。 建议通过复制和覆盖项目 `android` 目录中的相同文件来应用更改。 在覆盖之前,当前的项目状态应该被提交到源代码管理中,并检查建议的改动。 该工具不会自动更改 `android` 目录。

  3. 一旦生成可用的加载单元并将其记录到 `deferred_components_loading_units.yaml` 中, 便可完善 pubspec 的 `deferred-components` 配置,将加载单元分配给延迟的组件。 在上面的案例中,生成的 `deferred_components_loading_units.yaml` 文件将包含:

    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    复制代码

    加载单元 id(在本例中为「2」)由 Dart 内部使用,可以忽略。 基本加载单元(id 为「1」)包含了其他加载单元中未显式列出的所有内容,在这里没有列出。

    现在可以将以下内容添加到 pubspec.yaml 中:

    ...
    flutter:
      ...
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      ...
    复制代码

    将加载单元分配到延迟组件,把加载单元中的任何 Dart 库添加到功能模块的 libraries 部分。 请记住以下准则:

    • 一个加载单元只能包含在一个延迟组件中

    • 引用加载单元中的一个 Dart 库意味着整个加载单元都被包含在延迟组件中。

    • 所有未被分配给延迟组件的加载单元都包含在基本组件中,基本组件始终隐式存在。

    • 分配给同一延迟组件的加载单元将一起下载、安装和运行。

    • 基本组件是隐式的,不需要在 pubspec 中定义。

  4. 静态资源也可以通过在延迟组件中配置 assets 进行添加 :

      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
          assets:
            - assets/image.jpg
            - assets/picture.png
              # wildcard directory
            - assets/gallery/
    复制代码

    一个静态资源可以包含在多个延迟组件中,但是安装这两个组件会导致资源的重复。 也可以通过省略 libraries 来定义纯静态资源的延迟组件。 这些静态资源的组件必须与服务中的 DeferredComponent 实用程序类一起安装,而不是 loadLibrary()。 由于 Dart 库是与静态资源打包在一起的,因此如果用 loadLibrary() 加载 Dart 库,则也会加载组件中的所有资源。 但是,按组件名称安装和服务实用程序不会加载组件中的任何 Dart 库。

    你可以自由选择将资源包含在任何组件中,只要它们是在首次引用时安装和加载的, 但通常情况下,静态资源和使用这些资源的 Dart 代码最好打包在同一组件中。

  5. 将在 `pubspec.yaml` 中定义的所有延迟组件手动添加到 `android/settings.gradle` 文件中的 includes 部分。 例如,如果 pubspec 中定义了三个名为 `boxComponent`、 `circleComponent` 和 `assetComponent` 的延迟组件, 请确保 `android/settings.gradle` 中包含以下内容:

    include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
    ...
    复制代码
  6. 重复步骤 `3.1` 到 `3.6`(此步骤), 直到处理了所有验证程序的建议,并且该工具在没有更多建议的情况下运行。

    成功时,此命令将在 build/app/outputs/bundle/release 目录下输出 app-release.aab 文件。

    构建成功并非总是意味着应用是按预期构建的。 你需要确保所有的加载单元和 Dart 库都以你想要的方式包含在内。 例如,一个常见的错误是不小心导入了一个没有 deferred 关键字的 Dart 库, 导致一个延迟加载库被编译为基本加载单元的一部分。 在这种情况下,Dart 库将正确加载,因为它始终存在于基本组件中,并且库不会被拆分。 可以通过检查 deferred_components_loading_units.yaml 文件, 验证预期的加载单元是否生成描述。

    当调整延迟组件配置,或者进行添加、修改、删除加载单元的更改时, 你应该预料到验证程序会失败。按照步骤 3.13.6(此步骤)中的所有建议继续构建。

在本地运行应用

一旦你的应用程序成功构建了一个 .aab 文件, 就可以使用 Android 的 bundletool 来执行带有 --local testing 标志的本地测试。

要在测试设备上运行 .aab 文件,请从 github.com/google/bund… 下载 bundletool jar 可执行文件,然后运行:

$ java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing

$ java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks
复制代码

<your_app_project_dir> 是应用程序对应项目的目录位置, <your_temp_dir> 用于存储 bundletool 输出的所有临时目录。 这会将你的 .aab 文件解压为 .apks 文件并将其安装到设备上。 所有 Android 可用的动态特性都在本地加载到设备上,并模拟延迟组件的安装。

Before running build-apks again, remove the existing app .apks file:

再次运行 build-apks 之前,请删除已存在的 app.apks 文件:

$ rm <your_temp_dir>/app.apks
复制代码

对 Dart 代码库的更改需要增加 Android 构建 ID,或者卸载并重新安装应用程序。 因为除非检测到新的版本号,否则 Android 不会更新功能模块。

发布到 Google Play 商店

生成的 .aab 文件可以像平常一样直接上传到 Google Play 商店。 调用 loadLibrary() 时,Flutter 引擎将会使用从商店下载的包含 Dart AOT 库和资源的 Android 模块。


最后

动态化一直是移动端上的一个痛点,也是 Flutter 为人诟病的一点。2.2 之后官方直接提供了这一能力,可以让我们更轻松实现。基于这一特性,个人认为不止包体积优化,热修复应该也可以有相关的解决方案。虽然 Google play 在国内不可用,但整体流程任然可以参考本文,我们可以根据业务自定义延迟组件的下载与解压。我也会在下一篇翻译关于延迟组件的更多细节,就包含如何自定义延迟组件的管理程序。如果你也感兴趣欢迎关注,点赞,在评论区留下你的看法。

本文译者:Nayuta,贝壳找房高级 Android 工程师

致谢:

原作者:Gary Qian

审校::Vadaski,Flutter.cn 社区成员,就职于滴滴出行,以及来自社区的 CaijinglongAlexMeandNichenglu

往期优质专栏:

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

Flutter核心渲染机制

Flutter路由设计与源码解析

Flutter事件分发

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

文章分类
Android
文章标签