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

news2025/1/12 21:47:45

导读

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/,需要加权限的联系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

作者:京东物流 沈明亮

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/558132.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝精灵协会启动第二阶段的 NFT 连续发售活动

四个月前&#xff0c;蓝精灵协会推出了一款完全上链的 NFT 游戏&#xff0c;参与的钱包数量超过 85,000 个&#xff0c;并进入了前 100 Dapps 排名&#xff0c;成为了 Web3 领域的一匹黑马。 两周前&#xff0c;我们开始了第二阶段的连续销售活动&#xff0c;旨在建立一个前沿 …

privateGPT centos7环境下部署和研究

gihtub代码 https://github.com/imartinez/privateGPT 安装 llama-cpp-python-0.1.48安装报错 Could not build wheels for llama-cpp-python, , which is required to install pyproject.toml-based projects搜索&#xff08;结果较少&#xff09;&#xff1a; 从文章&#…

python数据分析与可视化学习简记(更新中)

numpy简介 numpy(Numerical Python)是python语言的扩展程序库&#xff0c;支持大量的维度数组与矩阵运算&#xff0c;此外也针对数组运算提供了大量的数学函数库 作用 Numpy是一个运行速度非常快的数学库&#xff0c;主要用于数组计算 安装 可以使用如下命令在命令行安装即可 p…

CnOpenData舆情云数据

一、数据简介 网络舆情监测数据是决策者进行数据分析和决策处置的基础。舆情云数据覆盖81000 网站、5600 论坛、1000 平面媒体、2500万 微信账号、3亿 微博账号、300 网络视频、17000 境外媒体、1400万 自媒体账号、2500 新闻客户端、170 电视台 &#xff0c;数据来源全面精准&…

Spring Boot 多环境配置

Spring Boot 多环境配置 在实际开发中&#xff0c;应用程序通常需要在不同的环境中运行&#xff0c;例如开发环境、测试环境和生产环境。每个环境可能需要不同的配置&#xff0c;包括数据库连接、日志级别、接口地址等等。Spring Boot 提供了多种方法来处理多环境配置&#xf…

【数据分享】1901-2021年1km分辨率逐月平均气温栅格数据(全国/分省/免费获取)

气温数据是我们最常用的气象指标之一&#xff0c;之前我们给大家分享过1950-2022年0.1 x 0.1精度的逐月平均气温栅格数据和逐年平均气温栅格数据&#xff08;均可查询之前的文章获悉详情&#xff09;&#xff01; 本次我们分享的是精度更高的气温栅格数据——1901-2021年1km分…

小兔鲜项目----Pinia

目录 Pinia快速入门 什么是Pinia 添加Pinia到Vue项目 使用Pinia实现计数器案例 getters实现 action如何实现异步 storeToRefs Pinia的调试 项目起步-初始化项目并使用git管理 创建项目并精细化配置 src目录调整 git 管理项目 项目起步-配置别名路径联想提示 什么是…

Cesium教程(四):调用地形服务

Cesium提供了多种现有地形数据服务接口&#xff0c;并且支持自定义地形数据。 CesiumTerrainProvider&#xff1a;高精度全球地形数据&#xff0c;附有光照和水面效果。地形切片使用quantized-mesh v1.0格式&#xff0c;Cesium使用CesiumTerrainProvider API调用该服务。 Goo…

git bash下载gitee项目报错:no such file found

错误再现 解决方案&#xff1a;设置SSH Key 第一步&#xff1a;右击git bash here&#xff0c;命令行输入 $ git config --global user.name “hcshow” //配置用户名 $ git config --global user.email “hcitlifehotmail.com” //配置用户邮箱地址 此时可以查看用户名等信息…

【C++ 学习 ⑦】- 模板初阶(函数模板和类模板)

目录 一、前言 二、函数模板 2.1 - 基本概念和原理 2.2 - 定义格式 2.3 - 实例化详解 2.3.1 - 隐式实例化 2.3.2 - 显示实例化 2.4 - 模板参数的匹配原则 三、类模板 3.1 - 定义格式 3.2 - 实例化 参考资料&#xff1a; C函数模板&#xff08;模板函数&#xff09;详…

chatgpt赋能Python-python_for_line_in_f

Python for Line in F: 一种高效的文件读写方法 对于Python编程工程师来说&#xff0c;文件的读写操作是一项必备技能。但是&#xff0c;如果你还在使用传统的逐行读取方法&#xff0c;那么你可能需要了解一种更加高效的文件读写方法&#xff1a;Python for Line in F。 什么…

使用亚马逊云科技Amazon VPC Lattice简化服务间的连接、安全和监控

在亚马逊云科技re:Invent 2022中&#xff0c;亚马逊云科技介绍了Amazon VPC Lattice预览版&#xff0c;这是Amazon Virtual Private Cloud&#xff08;Amazon VPC&#xff09;的一项新功能&#xff0c;可通过一致的方式连接、保护和监控服务之间的通信。借助Amazon VPC Lattice…

十八、map和set

文章目录 一、关联式容器&#xff08;一&#xff09;序列式容器&#xff1a;&#xff08;二&#xff09;关联式容器&#xff1a; 二、树形结构与哈希结构&#xff08;一&#xff09;树型结构&#xff08;二&#xff09;哈希结构 三、键值对四、set五、multiset六、map&#xff…

有没有一种支持对象建模、数据库建模和低代码能力的工具,用来解放程序员生产力呢?

1. 简介 作为一个程序员&#xff0c;很多时候在面对项目开发工期短、任务重、功能复杂、压力大&#xff0c;同时还得迎合领导或者甲方的要求提供研发设计文档时&#xff0c;往往很苦恼&#xff0c;因为随着软件项目的迭代&#xff0c;很难保持输出与代码一致的数据模型和架构模…

Configuring MySQL for a SSD based SAN

Configuring MySQL for a SSD based SAN (Doc ID 2673662.1)正在上传…重新上传取消To Bottom In this Document APPLIES TO: MySQL Server - Version 5.6 and later Information in this document applies to any platform. GOAL Considering a Virtualized environment wit…

chatgpt赋能Python-python_ip归属地

Python IP归属地查询 在网络安全领域&#xff0c;IP地址归属地查询是一项非常重要的任务。很多时候我们需要知道某个IP地址的归属地以解决一些安全问题。Python语言在这方面也发挥了巨大的作用&#xff0c;有各种成熟的IP地址归属地查询库。在本文中&#xff0c;我们将介绍如何…

CMOS摄像头驱动分析-i2c驱动

CMOS摄像头驱动分析-i2c驱动 文章目录 CMOS摄像头驱动分析-i2c驱动设备树内容module_i2c_driver宏分析ov2640_i2c_driverov2640_probe 设备树内容 ov2640: camera0x30 {compatible "ovti,ov2640";reg <0x30>;status "okay";pinctrl-names "…

三、Mybatis从入门到入土

一、什么是Mybatis 1&#xff09;MyBatis 是一款优秀的持久层&#xff08;DAO层&#xff09;框架 2&#xff09;MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程 3&#xff09;MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息&#xff0c;将…

程序员做爬虫有哪些痛点

很多网站会设置反爬虫机制&#xff0c;如验证码、IP封禁、请求频率限制等&#xff0c;这些机制会增加爬虫的难度。 程序员做爬虫时&#xff0c;可能会遇到以下难点&#xff1a; 1、反爬虫机制 许多网站会设置反爬虫机制&#xff0c;如验证码、IP封禁、请求频率限制等&#x…

ICV: ADAS SoC市场规模将在2024年迎来较大突破

随着先进驾驶辅助系统&#xff08;ADAS&#xff09;的出现和对于自动驾驶的追求&#xff0c;汽车行业正在经历快速转型。这些技术进步的核心是ADAS SoC&#xff0c;它是实现多个功能集成于单一平台的关键组件。ADAS SoC已经成为智能汽车的重要驱动因素&#xff0c;彻底改变了安…