写点什么

Deferred Components- 实现 Flutter 运行时动态下发 Dart 代码 | 京东云技术团队

  • 2023-05-23
    北京
  • 本文字数:7009 字

    阅读完需:约 23 分钟

Deferred Components-实现Flutter运行时动态下发Dart代码 | 京东云技术团队

导读

Deferred Components,官方实现的 Flutter 代码动态下发的方案。本文主要介绍官方方案的实现细节,探索在国内环境下使用 Deferred Components,并且实现了最小验证 demo。读罢本文,你就可以实现 Dart 文件级别代码的动态下发。

一、引言

Deferred Components 是 Flutter2.2 推出的功能,依赖于 Dart2.13 新增的对 Split AOT 编译支持。将可以在运行时每一个可单独下载的 Dart 库、assets 资源包称之为延迟加载组件,即 Deferred Components。Flutter 代码编译后,所有的业务逻辑都会打包在 libapp.so 一个文件里。但如果使用了延迟加载,便可以分拆为多个 so 文件,甚至一个 Dart 文件也可以编译成一个单独的 so 文件。


这样带来的好处是显而易见的,可以将一些不常用功能放到单独的 so 文件中,当用户使用时再去下载,可以大大降低安装包的大小,提高应用的下载转换率。另外,因为 Flutter 具备了运行时动态下发的能力,这让大家看到了实现 Flutter 热修复的另一种可能。截止目前来讲,官方的实现方案必须依赖 Google Play,虽然也针对中国的开发者给出了不依赖 Google Play 的自定义方案,但是并没有给出实现细节,市面上也没有自定义实现的文章。本文会先简单介绍官方实现方案,并探究其细节,寻找自定义实现的思路,最终会实现一个最小 Demo 供大家参考。

二、官方实现方案探究

2.1 基本步骤

2.1.1.引入 play core 依赖。


dependencies {  implementation "com.google.android.play:core:1.8.0"}
复制代码


2.1.2.修改 Application 类的 onCreate 方法和 attachBaseContext 方法。


@Overrideprotected void onCreate(){ super.onCreate()// 负责deferred components的下载与安装 PlayStoreDeferredComponentManager deferredComponentManager = new  PlayStoreDeferredComponentManager(this, null);FlutterInjector.setInstance(new FlutterInjector.Builder()    .setDeferredComponentManager(deferredComponentManager).build());}

@Overrideprotected void attachBaseContext(Context base) { super.attachBaseContext(base); // Emulates installation of future on demand modules using SplitCompat. SplitCompat.install(this);}
复制代码


2.1.3.修改 pubspec.yaml 文件。


flutter:    deferred-components:
复制代码


2.1.4.在 flutter 工程里新增 box.dart 和 some_widgets.dart 两个文件,DeferredBox 就是要延迟加载的控件,本例中 box.dart 被称为一个加载单元,即 loading_unit,每一个 loading_unit 对应唯一的 id,一个 deferred component 可以包含多个加载单元。记得这个概念,后续会用到。


// 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, ); }}
复制代码


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() { //只有调用了loadLibrary方法,才会去真正下载并安装deferred components. _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(); }, ); }}
复制代码


2.1.5.然后在 main.dart 里面新增一个跳转到 SomeWidget 页面的按钮。


 Navigator.push(context, MaterialPageRoute(      builder: (context) {        return const SomeWidget();      },    ));
复制代码


2.1.6.terminal 里运行 flutter build appbundle 命令。此时,gen_snapshot 不会立即去编译 app,而是先运行一个验证程序,目的是验证此工程是否符合动态下发 dart 代码的格式,第一次构建时肯定不会成功,你只需要按照编译提示去修改即可。当全部修改完毕后,会得到最终的.aab 类型的安装包。


以上便是官方实现方案的基本步骤,更多细节可以参考官方文档


https://docs.flutter.dev/perf/deferred-components

2.2 本地验证

在将生成的 aab 安装包上传到 Google Play 上之前,最好先本地验证一下。


首先你需要下载 bundletool,然后依次运行下列命令就可以将 aab 安装包装在手机上进行最终的验证了。


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
复制代码

2.3 loadLibrary()方法调用的生命周期


图 1 官方实现方案介绍图


(来源:https://github.com/flutter/flutter/wiki/Deferred-Components)


从官方的实现方案中可以知道,只有调用了 loadLibrary 方法后,才会去真正执行 deferred components 的下载与安装工作,现在着重看下此方法的生命周期。


调用完 loadLibrary 方法后,dart 会在内部查询此加载单元的 id,并将其一直向下传递,当到达 jni 层时,jni 负责将此加载单元对应的 deferred component 的名字以及此加载单元 id 一块传递给


PlayStoreDynamicFeatureManager,此类负责从 Google Play Store 服务器下载对应的 Deferred Components 并负责安装。安装完成后会逐层通知,最终告诉 dart 层,在下一帧渲染时展示动态下发的控件。

三、自定义实现

3.1 思路

梳理了 loadLibrary 方法调用的生命周期后,只需要自己实现一个类来代替


PlayStoreDynamicFeatureManager 的功能即可。在官方方案中具体负责完成 PlayStoreDynamicFeatureManager 功能的实体类是 io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager,其继承自 DeferredComponentManager,分析源码得知,它最重要的两个方法是 installDeferredComponent 和 loadDartLibrary。


  • installDeferredComponent:这个方法主要负责 component 的下载与安装,下载安装完成后会调用 loadLibrary 方法,如果是 asset-only component,那么也需要调用 DeferredComponentChannel.completeInstallSuccess 或者 DeferredComponentChannel.completeInstallError 方法。


  • loadDartLibrary:主要是负责找到 so 文件的位置,并调用 FlutterJNI dlopen 命令打开 so 文件,你可以直接传入 apk 的位置,flutterJNI 会直接去 apk 里加载 so,避免处理解压 apk 的逻辑。


那基本思路就有了,自己实现一个实体类,继承 DeferredComponentManager,实现这两个方法即可。

3.2 代码实现

本例只是最小 demo 实现,cpu 架构采用 arm64,且暂不考虑 asset-only 类型的 component。


3.2.1.新增


CustomDeferredComponentsManager 类,继承 DeferredComponentManager。


3.2.2.实现 installDeferredComponent 方法,将 so 文件放到外部 SdCard 存储里,代码负责将其拷贝到应用的私有存储中,以此来模拟网络下载过程。代码如下:


@Overridepublic void installDeferredComponent(int loadingUnitId, String componentName) {    String resolvedComponentName = componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);    if (resolvedComponentName == null) {         Log.e(TAG, "Deferred component name was null and could not be resolved from loading unit id.");         return;     }     // Handle a loading unit that is included in the base module that does not need download.     if (resolvedComponentName.equals("") && loadingUnitId > 0) {     // No need to load assets as base assets are already loaded.         loadDartLibrary(loadingUnitId, resolvedComponentName);         return;     }     //耗时操作,模拟网络请求去下载android module     new Thread(         () -> {//将so文件从外部存储移动到内部私有存储中              boolean result = moveSoToPrivateDir();              if (result) {                 //模拟网络下载,添加2秒网络延迟                 new Handler(Looper.getMainLooper()).postDelayed(                                () -> {                                    loadAssets(loadingUnitId, resolvedComponentName);                                    loadDartLibrary(loadingUnitId, resolvedComponentName);                                    if (channel != null) {                                        channel.completeInstallSuccess(resolvedComponentName);                                    }                                }                                , 2000);                 } else {                        new Handler(Looper.getMainLooper()).post(                                () -> {                                    Toast.makeText(context, "未在sd卡中找到so文件", Toast.LENGTH_LONG).show();

if (channel != null) { channel.completeInstallError(resolvedComponentName, "未在sd卡中找到so文件"); }

if (flutterJNI != null) { flutterJNI.deferredComponentInstallFailure(loadingUnitId, "未在sd卡中找到so文件", true); } } ); } } ).start(); }
复制代码


3.2.3.实现 loadDartLibrary 方法,可以直接拷贝


PlayStoreDeferredComponentManager 类中的此方法,注释已加,其主要作用就是在内部私有存储中找到 so 文件,并调用 FlutterJNI dlopen 命令打开 so 文件。


  @Override    public void loadDartLibrary(int loadingUnitId, String componentName) {        if (!verifyJNI()) {            return;        }        // Loading unit must be specified and valid to load a dart library.        //asset-only的component的unit id为-1,不需要加载so文件        if (loadingUnitId < 0) {            return;        }

//拿到so的文件名字 String aotSharedLibraryName = loadingUnitIdToSharedLibraryNames.get(loadingUnitId); if (aotSharedLibraryName == null) { // If the filename is not specified, we use dart's loading unit naming convention. aotSharedLibraryName = flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so"; }

//拿到支持的abi格式--arm64_v8a // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64 String abi; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { abi = Build.SUPPORTED_ABIS[0]; } else { abi = Build.CPU_ABI; } String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.

// TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more // performant and robust.

// Search directly in APKs first List<String> apkPaths = new ArrayList<>(); // If not found in APKs, we check in extracted native libs for the lib directly. List<String> soPaths = new ArrayList<>();

Queue<File> searchFiles = new LinkedList<>(); // Downloaded modules are stored here--下载的 modules 存储位置 searchFiles.add(context.getFilesDir()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //第一次通过appbundle形式安装的split apks位置 // The initial installed apks are provided by `sourceDirs` in ApplicationInfo. // The jniLibs we want are in the splits not the baseDir. These // APKs are only searched as a fallback, as base libs generally do not need // to be fully path referenced. for (String path : context.getApplicationInfo().splitSourceDirs) { searchFiles.add(new File(path)); } }

//查找apk和so文件 while (!searchFiles.isEmpty()) { File file = searchFiles.remove(); if (file != null && file.isDirectory() && file.listFiles() != null) { for (File f : file.listFiles()) { searchFiles.add(f); } continue; } String name = file.getName(); // Special case for "split_config" since android base module non-master apks are // initially installed with the "split_config" prefix/name. if (name.endsWith(".apk") && (name.startsWith(componentName) || name.startsWith("split_config")) && name.contains(pathAbi)) { apkPaths.add(file.getAbsolutePath()); continue; } if (name.equals(aotSharedLibraryName)) { soPaths.add(file.getAbsolutePath()); } }

List<String> searchPaths = new ArrayList<>();

// Add the bare filename as the first search path. In some devices, the so // file can be dlopen-ed with just the file name. searchPaths.add(aotSharedLibraryName);

for (String path : apkPaths) { searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName); } for (String path : soPaths) { searchPaths.add(path); }//打开so文件 flutterJNI.loadDartDeferredLibrary(loadingUnitId, searchPaths.toArray(new String[searchPaths.size()])); }
复制代码


3.2.4.修改 Application 的代码并删除


com.google.android.play:core 的依赖。


override fun onCreate() {        super.onCreate()        val deferredComponentManager = CustomDeferredComponentsManager(this, null)        val injector = FlutterInjector.Builder().setDeferredComponentManager(deferredComponentManager).build()        FlutterInjector.setInstance(injector)
复制代码


至此,核心代码全部实现完毕,其他细节代码可以见


https://coding.jd.com/jd_logistic/deferred_component_demo/,需要加权限的联系shenmingliang1即可。

3.3 本地验证

  • 运行 flutter build appbundle --release --target-platform android-arm64 命令生成 app-release.aab 文件。

  • .运行下列命令将 app-release.aab 解析出本地可以安装的 apks 文件:java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks --local-testing

  • 解压上一步生成的 app.apks 文件,在加压后的 app 文件夹下找到 splits/scoreComponent-arm64_v8a_2.apk,继续解压此 apk 文件,在生成的 scoreComponent-arm64_v8a_2 文件夹里找到 lib/arm64-v8a/libapp.so-2.part.so 文件。

  • 执行 java -jar bundletool.jar install-apks --apks=app.apks 命令安装 app.apks,此时打开安装后的 app,点击首页右下角的按钮跳转到 DeferredPage 页面,此时页面不会成功加载,并且会提示你“未在 sd 卡中找到 so 文件”。

  • 将第 3 步找到的 lipase.so-2.part.so push 到指定文件夹下,命令如下 adb push libapp.so-2.part.so /storage/emulated/0/Android/data/com.example.deferred_official_demo/files。重启 app 进程,并重新打开 DeferredPage 界面即可。

四、 总结

官方实现方案对国内的使用来讲,最大的限制无疑是 Google Play,本文实现了一个脱离 Google Play 限制的最小 demo,验证了 deferred components 在国内使用的可行性。


参考:


  1. https://docs.flutter.dev/perf/deferred-components

  2. https://github.com/flutter/flutter/wiki/Deferred-Components


作者:京东物流 沈明亮

内容来源:京东云开发者社区

发布于: 3 小时前阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Deferred Components-实现Flutter运行时动态下发Dart代码 | 京东云技术团队_flutter_京东科技开发者_InfoQ写作社区