乐趣区

关于前端: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 办法。

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


@Override
protected 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 存储里,代码负责将其拷贝到利用的公有存储中,以此来模仿网络下载过程。代码如下:

@Override
public 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/,…

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

作者:京东物流 沈亮堂

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

退出移动版