跳到主内容

Android 和 Web 的延迟加载组件

如何创建延迟加载组件以提高下载性能。

介绍

#

使用 Flutter,Android 和 Web 应用可以在运行时下载延迟加载组件(额外的代码和资源)。如果您的应用体积较大,并且只想在用户确实需要时才安装某些组件,这将非常有用。

虽然 Flutter 支持 Android 和 Web 上的延迟加载,但实现方式有所不同。两者都需要使用 Dart 的延迟导入 (deferred imports)

  • Android 的 动态功能模块 (dynamic feature modules) 以 Android 模块的形式交付延迟加载组件。

    构建 Android 应用时,尽管可以延迟加载模块,但您必须构建整个应用并将其作为单个 Android App Bundle (AAB) 上传。Flutter 不支持在不重新上传整个应用的新 Android App Bundle 的情况下分发部分更新。

    当您在 release 或 profile 模式下编译 Android 应用时,Flutter 会执行延迟加载,而 debug 模式会将所有延迟加载组件视为常规导入。

  • Web 平台会将延迟加载组件创建为单独的 *.js 文件。

有关此功能技术细节的深入探讨,请参阅 Flutter Wiki 上的 Deferred Components

如何为延迟加载组件设置 Android 项目

#

以下说明解释了如何为 Android 应用设置延迟加载。

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

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

    android/app/build.gradle.kts
    kotlin
    ...
    dependencies {
      ...
      implementation("com.google.android.play:core:1.8.0")
      ...
    }
    
    android/app/build.gradle
    groovy
    ...
    dependencies {
      ...
      implementation "com.google.android.play:core:1.8.0"
      ...
    }
    
  2. 如果使用 Google Play 商店作为动态功能的发布模型,应用必须支持 SplitCompat 并提供 PlayStoreDeferredComponentManager 的实例。这两项任务都可以通过在 android/app/src/main/AndroidManifest.xml 中将 application 的 android:name 属性设置为 io.flutter.embedding.android.FlutterPlayStoreSplitApplication 来完成。

    xml
    <manifest ...
      <application
         android:name="io.flutter.embedding.android.FlutterPlayStoreSplitApplication"
            ...
      </application>
    </manifest>
    

    io.flutter.app.FlutterPlayStoreSplitApplication 会为您处理这两项任务。如果您使用 FlutterPlayStoreSplitApplication,可以直接跳到第 1.3 步。

    如果您的 Android 应用规模庞大或结构复杂,您可能需要手动支持 SplitCompat 并提供 PlayStoreDynamicFeatureManager

    要支持 SplitCompat,有三种方法(详细信息请参阅 Android 文档),以下任一方法均有效:

    • 使您的 Application 类继承 SplitCompatApplication

      java
      public class MyApplication extends SplitCompatApplication {
          ...
      }
      
    • attachBaseContext() 方法中调用 SplitCompat.install(this);

      java
      @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 类中

      xml
      <application
          ...
          android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
      </application>
      

    嵌入器依赖于注入的 DeferredComponentManager 实例来处理延迟加载组件的安装请求。通过在应用初始化时添加以下代码,为 Flutter 嵌入器提供一个 PlayStoreDeferredComponentManager

    java
    import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDeferredComponentManager;
    import io.flutter.FlutterInjector;
    ...
    PlayStoreDeferredComponentManager deferredComponentManager = new
      PlayStoreDeferredComponentManager(this, null);
    FlutterInjector.setInstance(new FlutterInjector.Builder()
        .setDeferredComponentManager(deferredComponentManager).build());
    
  3. 通过在 pubspec.yamlflutter 条目下添加 deferred-components 条目,来启用延迟加载组件

    yaml
    ...
    flutter:
      ...
      deferred-components:
      ...
    

    flutter 工具会查找 pubspec.yaml 中的 deferred-components 条目,以确定是否应将应用构建为延迟加载模式。除非您已经确定了所需的组件以及包含在其中的 Dart 延迟库,否则此部分目前可以留空。一旦 gen_snapshot 生成了加载单元,您稍后将在 第 3.3 步 中填充此部分。

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

#

接下来,在您的应用 Dart 代码中实现延迟加载的 Dart 库。实现方案不需要即刻完成功能。本页其余部分的示例添加了一个新的简单延迟加载小部件作为占位符。您也可以通过修改导入并将延迟代码的使用限制在 loadLibrary()Future 中,将现有代码转换为延迟加载。

  1. 创建一个新的 Dart 库。例如,创建一个可在运行时下载的新 DeferredBox 小部件。该小部件可以具有任意复杂度,但为了本指南的目的,请创建一个简单的方块作为替身。要创建一个简单的蓝色方块小部件,请创建包含以下内容的 box.dart

    box.dart
    dart
    import 'package:flutter/material.dart';
    
    /// A simple blue 30x30 box.
    class DeferredBox extends StatelessWidget {
      const DeferredBox({super.key});
    
      @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 代码。

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

    loadLibrary() 函数返回一个 Future<void>,当库中的代码可供使用时,它会成功完成,否则会以错误结束。对延迟库中符号的所有使用都应受保护在已完成的 loadLibrary() 调用之后。该库的所有导入都必须标记为 deferred,以便将其正确编译并用于延迟加载组件。如果组件已经加载,则后续对 loadLibrary() 的调用会很快完成(但不是同步的)。loadLibrary() 函数也可以提前调用以触发预加载,从而帮助掩盖加载时间。

    您可以在 Flutter Gallery 的 lib/deferred_widget.dart 中找到另一个延迟导入加载的示例。

第 3 步:构建应用

#

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

flutter build appbundle

此命令会验证您的项目是否已正确设置为构建延迟组件应用。默认情况下,如果验证程序检测到任何问题,构建将失败,并引导您进行建议的更改以修复它们。

  1. flutter build appbundle 命令会运行验证程序,并尝试指示 gen_snapshot 构建应用,从而生成拆分的 AOT 共享库作为单独的 SO 文件。在第一次运行时,验证程序很可能会失败,因为它会检测到问题;该工具会提供有关如何设置项目和修复这些问题的建议。

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

    验证程序会检测 gen_snapshot 生成的任何新的、更改的或删除的加载单元。当前生成的加载单元会在您的 <projectDirectory>/deferred_components_loading_units.yaml 文件中进行跟踪。该文件应检入源代码管理,以确保其他开发人员对加载单元的更改能被及时发现。

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

    • <projectDir>/android/app/src/main/res/values/strings.xml
      为每个延迟组件创建一个条目,将键 ${componentName}Name 映射到 ${componentName}。此字符串资源由每个功能模块的 AndroidManifest.xml 用于定义 dist:title 属性。例如:

      xml
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
        ...
        <string name="boxComponentName">boxComponent</string>
      </resources>
      
    • <projectDir>/android/<componentName>
      每个延迟组件都存在一个 Android 动态功能模块,其中包含 build.gradlesrc/main/AndroidManifest.xml 文件。这仅检查是否存在,而不验证这些文件的内容。如果文件不存在,它会生成一个默认的建议文件。

    • <projectDir>/android/app/src/main/res/values/AndroidManifest.xml
      包含一个 meta-data 条目,用于编码加载单元与该加载单元关联的组件名称之间的映射。该映射由嵌入器使用,以便将 Dart 的内部加载单元 ID 转换为要安装的延迟组件的名称。例如:

      xml
      ...
      <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. 对于这些检查中的每一项,该工具都会生成通过检查所需的修改或新文件。这些文件被放置在 <projectDir>/build/android_deferred_components_setup_files 目录中。建议通过复制并覆盖项目 android 目录中的同名文件来应用这些更改。在覆盖之前,应将当前项目状态提交到源代码管理,并审查建议的更改是否合适。该工具不会自动对您的 android/ 目录进行任何更改。

  3. 一旦生成的可用加载单元记录在 <projectDirectory>/deferred_components_loading_units.yaml 中,就可以完全配置 pubspec 的 deferred-components 部分,以便将加载单元分配给所需的延迟组件。继续上面的 box 示例,生成的 deferred_components_loading_units.yaml 文件将包含:

    yaml
    loading-units:
      - id: 2
        libraries:
          - package:MyAppName/box.Dart
    

    加载单元 ID(在本例中为 '2')由 Dart 内部使用,可以忽略。基础加载单元(ID '1')未列出,包含未明确包含在其他加载单元中的所有内容。

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

    yaml
    ...
    flutter:
      ...
      deferred-components:
        - name: boxComponent
          libraries:
            - package:MyAppName/box.Dart
      ...
    

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

    • 加载单元不应包含在多个组件中。

    • 包含来自加载单元的一个 Dart 库意味着整个加载单元都被分配给该延迟组件。

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

    • 分配给同一延迟组件的加载单元会被一起下载、安装和分发。

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

  4. 也可以通过在延迟组件配置中添加 assets 部分来包含资源:

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

    资源可以包含在多个延迟组件中,但安装这两个组件会导致资源重复。也可以通过省略 libraries 部分来定义仅包含资源的组件。这些仅包含资源的组件必须使用 services 中的 DeferredComponent 实用程序类而不是 loadLibrary() 进行安装。由于 Dart 库与资源打包在一起,如果使用 loadLibrary() 加载 Dart 库,组件中的任何资源也会被加载。但是,按组件名称和 services 实用程序安装将不会加载组件中的任何 Dart 库。

    您可以随意在任何组件中包含资源,只要它们在首次引用时被安装和加载即可,尽管通常情况下,资源和使用这些资源的 Dart 代码最好打包在同一个组件中。

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

    android/settings.gradle.kts
    kotlin
    include(":app", ":boxComponent", ":circleComponent", ":assetComponent")
    ...
    
    android/settings.gradle
    groovy
    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 文件来验证生成的加载单元是否符合预期。

    当调整延迟组件配置,或进行添加、修改或删除加载单元的 Dart 更改时,验证程序可能会失败。请遵循步骤 3.1 到 3.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 文件解压为 APK 文件并安装在设备上。所有可用的 Android 动态功能都会在本地加载到设备上,并模拟延迟组件的安装。

在再次运行 build-apks 之前,请移除现有的应用 APK 文件。

rm <your_temp_dir>/app.apks

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

发布到 Google Play 商店

#

构建好的 AAB 文件可以像往常一样直接上传到 Play 商店。当调用 loadLibrary() 时,Flutter 引擎会使用 Play 商店的分发功能下载包含 Dart AOT 库和资源的 Android 模块。