查看原文
其他

Flutter 延迟加载组件:包体积优化、动态化

The following article is from 进击的Flutter Author Nayuta

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

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

本文原作:Google 工程师 Gary Qian

简介

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

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

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

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

下面的引导将介绍如何设置 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),其中任何一种都是有效的:

    嵌入层依赖注入的 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());

    • 让你的 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>

    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 等待 loadLibraryFuture 对象(在 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 目录中的以下内容:

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

    • 每个延迟组件名称的键值对映射 {componentName}

       每个功能模块的 AndroidManifest.xml 使用此字符串资源来定义 dist:title property例如:

    1. <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
    • 每个延迟组件都有一个 Android 动态功能模块,它包含一个 build.gradlesrc/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>
          ...

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

  2. 一旦生成可用的加载单元并将其记录到 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 中定义。
  3. 静态资源也可以通过在延迟组件中配置 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 代码最好打包在同一组件中。

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

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

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

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

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

  6. 在本地运行应用

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

    要在测试设备上运行 .aab 文件,请从 github.com/google/bundletool/releases 下载 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 在国内不可用,但整体流程任然可以参考本文,我们可以根据业务自定义延迟组件的下载与解压。我也会在下一篇翻译关于延迟组件的更多细节,就包含如何自定义延迟组件的管理程序。如果你也感兴趣欢迎关注,点赞,在评论区留下你的看法。

    - EOF -


    推荐阅读  点击标题可跳转

    1、一次生产环境 NPE 崩溃的排查记录

    2、Jetpack太香了,系统App也想用,怎么办?

    3、卡顿、ANR、死锁,线上如何监控?



    看完本文有收获?请分享给更多人

     推荐关注「安卓开发精选」,提升安卓开发技术

    点赞和在看就是最大的支持❤️

    : . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存