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 = newPlayStoreDeferredComponentManager(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.dartimport 'package:flutter/widgets.dart';/// A simple blue 30x30 box.
class DeferredBox extends StatelessWidget {DeferredBox() {}@overrideWidget 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;@overridevoid initState() {//只有调用了loadLibrary方法,才会去真正下载并安装deferred components._libraryFuture = box.loadLibrary();super.initState();}@overrideWidget 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-testingjava -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 modulenew 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文件。
@Overridepublic 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, mips64String 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 firstList<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在国内使用的可行性。
参考:
- https://docs.flutter.dev/perf/deferred-components
- https://github.com/flutter/flutter/wiki/Deferred-Components
作者:京东物流 沈明亮
内容来源:京东云开发者社区
相关文章:

Deferred Components-实现Flutter运行时动态下发Dart代码 | 京东云技术团队
导读 Deferred Components,官方实现的Flutter代码动态下发的方案。本文主要介绍官方方案的实现细节,探索在国内环境下使用Deferred Components,并且实现了最小验证demo。读罢本文,你就可以实现Dart文件级别代码的动态下发。 一、…...
08 集合框架1
什么是数据结构? 存储数据,组织数据的方法,就是对数据做增删改查的操作 常见的数据结构有哪些?各自的优缺点是什么? 数组:擅长修改 查找操作,不擅长增加 删除操作 链表:有单项链表和双向链表,擅长增加和删除操作,不擅长修改和查找的操作 队列:擅长操作头和尾,先进先出,…...

内卷把同事逼成了“扫地僧”,把Git上所有面试题整理成足足24W字测试八股文
互联网大厂更多的是看重学历还是技术? 毫无疑问,是技术,技术水平相近的情况下,肯定学历高/好的会优先一点,这点大家肯定都理解。 说实话,学弟学妹们找工作难,作为面试官招人也难呀!…...
10-jQuery-遍历children、parent、for、each、for...of等
1、for 循环:可以用来遍历数组或类数组对象,但不能用来遍历普通对象。 <ul><li>John</li><li>Doe</li><li>Jane</li><li>Doe</li> </ul><script>var lis $(li);for (var i 0; i &…...

联想集团财报:收入持续下滑,联想集团财务前景已恶化
来源:猛兽财经 作者:猛兽财经 联想集团2023财年第三季度财务业绩回顾 联想集团(00992)于2023年2月16日盘后公布了该公司2023财年第三季度的财报。 财报显示,联想集团的收入已经从2022财年第三季度的201.27亿美元下降到…...

GPT4限制被破解!ChatGPT实现超长文本处理的新方法
目录 前言 使用chat-gpt过程中有哪些痛点 1.无法理解人类情感和主观性 2.上下文丢失 3.约定被打断 那如何去解决这个痛点 Transformer(RMT)怎么去实现的 1.Transformer 模型 2.RMT模型 3.计算推理速率 4.渐进学习能力 总结 写到最后 大家好…...
奋斗,然后成功:我的架构狮之梦
与代码结缘 2018年,当时听说了一个很厉害的人——吴瀚清老师,也就是大家所熟知的“道哥”。关于他的事情有很多传说,于是我也很快成为了他的小迷弟,把吴瀚清老师当成了自己的偶像。 也是那一年,我买了人生中第一本关…...

自定义属性,v-bind computed的使用
0.0 自定义组件的使用 【掌握】 先自定义自己的组件 引入组件 import 组件名 from 路径/文件名 注册组件 <script> export default {components:{ // 组件注册组件名:组件名,组件名1},data(){ // 数据return {}},methods:{ // 方法} } </script&…...

解决城市内涝的措施有哪些?需要用到哪些监测设备?
随着城市化的不断推进,城市内涝问题日益凸显。极端天气事件如暴雨、台风等对城市基础设施和居民生活造成了严重影响。那么,解决城市内涝的措施有哪些?需要用到哪些监测设备?针对上述问题,本文会为大家一一进行讲解。 解决城市内涝的措施有哪…...

Spark大数据处理讲课笔记----Spark任务调度
零、本节学习目标 理解DAG概念了解Stage划分了解RDD在Spark中的运行流程 一、有向无环图 (一)DAG概念 DAG(Directed Acyclic Graph)叫做有向无环图,Spark中的RDD通过一系列的转换算子操作和行动算子操作形成了一个…...

【22-23春】AI作业10-经典卷积网络
1.LeNet & MNIST LeNet是一种神经网络的模型,用于图像识别和分类。他包含 3 个卷积层,2 个池化层,1 个全连接层。其中所有卷积层的所有卷积核都为 5x5,步长 strid1,池化方法都为全局 pooling,激活函数…...

第13章_约束
第13章_约束 1. 约束(constraint)概述 1.1 为什么需要约束 数据完整性(Data Integrity)是指数据的精确性(Accuracy)和可靠性(Reliability)。它是防止数据库中存在不符合语义规定的数据和防止因错误信息的…...
GPC规范--安全域基础概念
概述: 分为三种主流类型: 1、发卡方安全域(Issuer Security Domain, ISD),卡片上首要的、强制性存在的安全域,是卡片管理者(通常是发卡方)在卡片内的代表; 2、补充安全域(Supplementary Security Domain&am…...

C++初阶--C++入门之基础学习
0.前言 C是一门非常好的编程语言,但可能在学习C的过程中会遇到很多困难。人们常说 “一个人走得很快,一群人会走的更远”, 所以就让我们一起攻坚克难,一起征服C吧!从本章开始,我们将开始C的基础学习&#x…...

服务器虚拟化部署
服务器虚拟化部署 1、背景2、目的3、环境4、部署4.1、部署VMware ESXi4.1.1、准备工作4.1.2、部署ESXi4.1.3、配置ESXi4.1.4 、部署虚拟机 1、背景 项目上利旧9台服务器,项目需要使用15台服务器,外购已经没有项目硬件采购预算,只能从目前的…...
实验篇(7.2) 01. 实验环境介绍 远程访问 ❀ Fortinet网络安全专家 NSE4
【简介】学习NSE4,如果只看文章而不动手做实验,就象耍流氓。为了有效的巩固学习到的内容,建议经常动手做实验。实验不怕出错,身经百战后,再在生产环境部署和配置FortiGate防火墙,就会做到胸有成竹。 虚拟实…...

ThinkPHP6 模型层的模型属性,表映射关系,以及如何在控制层中使用模型层和模型层中的简单CRUD
ThinkPHP6 模型层的模型属性,表映射关系,以及模型层的CRUD及如何在控制层中使用模型层 1. model 模型层的默认映射规则 模型,即mvc模式中的model层,model层用来对接数据库,操作数据库的增删改查。 在tp6中ÿ…...

CodeForces.1806A .平面移动.[简单][判断可达范围][找步数规律]
题目描述: 题目解读: 给定移动规则以及起始点,终点;分析终点是否可达,可达则输出最小步数。 解题思路: 首先要判定是否可达。画图可知,对于题目给定的移动规则,只能到达起始点(a,b…...

Linux系统编程学习 NO.4 ——基础指令学习、操作系统时间的概念、文件压缩包的概念
1.时间相关的概念以及指令 1.1.时间相关的指令 1.1.1.date指令 date可以指定时间显示的格式:date 指定格式 选项 %H:小时 %M:分钟 %S:秒数 %Y:年份 %m:月份 %d:日 %F:相当于%Y-%m-%d %X:相当…...
leecode 数据库:601. 体育馆的人流量
导入数据: Create table If Not Exists Stadium (id int, visit_date DATE NULL, people int); Truncate table Stadium; insert into Stadium (id, visit_date, people) values (1, 2017-01-01, 10); insert into Stadium (id, visit_date, people) values (2, 20…...

第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...

人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...

热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...