西安网站建设聂卫,网站制作代码,微信网站建设公司首选,云南城乡建设网站导读
Deferred Components#xff0c;官方实现的Flutter代码动态下发的方案。本文主要介绍官方方案的实现细节#xff0c;探索在国内环境下使用Deferred Components#xff0c;并且实现了最小验证demo。读罢本文#xff0c;你就可以实现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 StateSomeWidget {Futurevoid _libraryFuture;overridevoid initState() {//只有调用了loadLibrary方法才会去真正下载并安装deferred components._libraryFuture box.loadLibrary();super.initState();}overrideWidget build(BuildContext context) {return FutureBuildervoid(future: _libraryFuture,builder: (BuildContext context, AsyncSnapshotvoid 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 --bundleyour_app_project_dir/build/app/outputs/bundle/release/app-release.aab --outputyour_temp_dir/app.apks --local-testingjava -jar bundletool.jar install-apks --apksyour_temp_dir/app.apks2.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 darts 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 firstListString apkPaths new ArrayList();// If not found in APKs, we check in extracted native libs for the lib directly.ListString soPaths new ArrayList();QueueFile 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());}}ListString 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 --bundleapp-release.aab --outputapp.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 --apksapp.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-componentshttps://github.com/flutter/flutter/wiki/Deferred-Components 作者京东物流 沈明亮 内容来源京东云开发者社区