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:依赖项和初始项目设置
将 Play Core 添加到 Android 应用程序的 build.gradle 依赖项中。 在
android/app/build.gradle
中添加以下内容:...
dependencies {
...
implementation "com.google.android.play:core:1.8.0"
...
}如果使用 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
后面的延迟加载代码的导入和保护用法,将现有代码转换为延迟代码。
创建新的 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,
);
}
}在应用中使用
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:
。
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
。例如:
<?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>
...
对于每个检查,该工具会创建或者修改需要的文件。 这些文件放在 /build/android_deferred_components_setup_files
目录下。 建议通过复制和覆盖项目 android
目录中的相同文件来应用更改。 在覆盖之前,当前的项目状态应该被提交到源代码管理中,并检查建议的改动。 该工具不会自动更改 android
目录。
一旦生成可用的加载单元并将其记录到 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 中定义。
静态资源也可以通过在延迟组件中配置 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 代码最好打包在同一组件中。
将在 pubspec.yaml
中定义的所有延迟组件手动添加到 android/settings.gradle
文件中的 includes 部分。 例如,如果 pubspec 中定义了三个名为 boxComponent
、 circleComponent
和 assetComponent
的延迟组件, 请确保 android/settings.gradle
中包含以下内容:
include ':app', ':boxComponent', ':circleComponent', ':assetComponent'
...
重复步骤 3.1
到 3.6
(此步骤), 直到处理了所有验证程序的建议,并且该工具在没有更多建议的情况下运行。
成功时,此命令将在 build/app/outputs/bundle/release
目录下输出 app-release.aab
文件。
构建成功并非总是意味着应用是按预期构建的。你需要确保所有的加载单元和 Dart 库都以你想要的方式包含在内。例如,一个常见的错误是不小心导入了一个没有 deferred
关键字的 Dart 库, 导致一个延迟加载库被编译为基本加载单元的一部分。在这种情况下,Dart 库将正确加载,因为它始终存在于基本组件中,并且库不会被拆分。可以通过检查 deferred_components_loading_units.yaml
文件, 验证预期的加载单元是否生成描述。
当调整延迟组件配置,或者进行添加、修改、删除加载单元的更改时, 你应该预料到验证程序会失败。按照步骤 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
文件解压为 .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 -
看完本文有收获?请分享给更多人
推荐关注「安卓开发精选」,提升安卓开发技术
点赞和在看就是最大的支持❤️