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

作者:京东物流 沈亮堂

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理