Flutter动态化探索
前言
Flutter的动态化,对于Android而言,一个很清晰的思路就是动态替换flutter_assets
的所有资源文件,因为Flutter加载代码和资源的工作目录即是应用沙盒目录下的app_flutter
目录,我们把这个目录下的文件进行对应替换即可,而对于IOS,由于本身系统的限制,官方目前也没相应方案,所以目前暂且说下Android平台上的Dynamic Patch
而目前Flutter Engine最新的Master分支上支持Flutter引擎的动态更新,所以Dynamic Patch支持JIT与AOT模式下的所有代码产物与资源的动态更新,以及模式互切,即下述文件:
isolate_snapshot_data :App代码数据段
isolate_snapshot_instr :App代码指令段
vm_snapshot_data :VM虚拟机数据段
vm_snapshot_instr :VM虚拟机指令段
kernel_blob.bin :Dart代码产物
flutter.so :Flutter引擎
assets :资源文件
在进行Flutter初始化时,做了动态加载Flutter引擎的支持:
if (sResourceUpdater == null) {
System.loadLibrary("flutter");
} else {
sResourceExtractor.waitForCompletion();
File lib = new File(PathUtils.getDataDirectory(applicationContext), DEFAULT_LIBRARY);
if (lib.exists()) {
System.load(lib.getAbsolutePath());
} else {
System.loadLibrary("flutter");
}
}
以及,对于icudtl.dat
字符编码表打进了flutter.so
引擎中,这一改变主要是减少了每次打包需要将icudtl.dat
它复制到flutter_assets
中维护成本,以及避免了Flutter在加载时把它拷贝到app_flutter
时发生错误的风险,这对动态更新也更加方便了,Flutter引擎即包含了它,构建脚本如下:
action("icudtl_object") {
script = "$flutter_root/sky/tools/objcopy.py"
icudtl_input = "//third_party/icu/flutter/icudtl.dat"
icudtl_output = "$root_build_dir/flutter_icu/icudtl.o"
inputs = [
"$icudtl_input",
]
outputs = [
"$icudtl_output",
]
args = [
"--objcopy", rebase_path(android_objcopy),
"--input", rebase_path(icudtl_input),
"--output", rebase_path(icudtl_output),
"--arch", current_cpu,
]
}
Flutter官方动态化流程
相关配置
Config Patch Server Url
Application节点下的MetaData属性,对应Key为PatchServerURL
uri = new URI(metaData.getString("PatchServerURL") + "/" + getAPKVersion() + ".zip");
Config Patch Download Mode
Application节点下的MetaData属性,对应Key为PatchDownloadMode
String patchDownloadMode = metaData.getString("PatchDownloadMode");
Config Patch Install Mode
Application节点下的MetaData属性,对应Key为PatchInstallMode
String patchInstallMode = metaData.getString("PatchInstallMode");
Patch Installed Path
public File getInstalledPatch() {
return new File(context.getFilesDir().toString() + "/patch.zip");
}
Patch Downloaded Path
File getDownloadedPatch() {
return new File(getInstalledPatch().getPath() + ".install");
}
动态更新流程
Flutter Dynamic Patch全局是通过一个开关判断是否开启了Patch更新特性,即application中的meta-data属性:DynamicPatching
官方内部动态更新流程大致为:
image
Flutter初始化主要流程为:
1、根据所配置项下载Patch文件(异步和同步两种方式)
2、检测Patch的下载目录是否有patch.zip.install
待安装的Patch文件
3、校验待安装Patch文件内的isolate等文件CRC32与构建号是否与App的一致
4、在上一步校验成功后,会把待安装的Patch文件重命名并拷贝到Patch安装目录,文件名为patch.zip
5、校验App下的data/packageName/app_flutter/目录的时间戳文件
6、在上一步校验成功后则删除该目录下的所有数据、指令集、flutter引擎文件,同时解压patch.zip
文件,获取下列文件拷贝至app_flutter目录:
libflutter.so
flutter_assets/app.flx(Deprecated)
flutter_assets/vm_snapshot_data
flutter_assets/vm_snapshot_instr
flutter_assets/isolate_snapshot_data
flutter_assets/isolate_snapshot_instr
flutter_assets/kernel_blob.bin
如果其中存在有些文件不存在patch.zip
中,则默认取Apk中assets
目录下对应的资源进行补充
7、在App下的app_flutter目录下重新生成时间戳文件,文件名称格式为:
res_timestamp-${App.versionCode}-${App.lastUpdateTime}-[${PatchNumber}]-${PatchFile.lastModifiedTime}
8、通过System.loadLibrary动态选择加载flutter.so
引擎,如启用了动态更新功能,则首先从app_flutter路径加载,否则默认加载App内的Flutter引擎,届时,Flutter的启动初始化完成
9、最后在App运行期如果有FlutterActivity页面的启动,则会进行Flutter引擎的初始化,并把AppBundle
路径(即app_flutter)传递给Flutter引擎供加载Flutter代码和数据资源,即:NativeInit
定制化动态化方案
为什么要屏蔽官方的流程,而自定义一套动态化部署流程?
官方的流程以及配置太过于硬核,不能很好的与当前的业务以及动态部署模式结合,如:不支持灰度发布、定向版本部署等
而Flutter对于自定义动态化流程,并未给出对应接口实现,如:Patch下载、校验规则、Patch替换等,所以对于使用我们自己的自定义的动态化流程,需要尽量不改动源码,保证侵入性最小,官方给出了一个metadata的配置来关闭或打开Patch的更新功能DynamicPatching
,首先关闭这个,不使用内部的动态更新Patch逻辑,其次,对于动态替换,保持让Flutter先执行初始化替换流程,而自定义的这套的动态替换流程在FlutterMain init之后进行初始化或配置,进而进行资源的替换更新,而flutter.so
的加载过程还在内部,这段需要屏蔽,从而动态加载我们下发的Flutter引擎,自定义的动态化加载方案一些流程和校验规则是可以参考官方实现
Flutter差量更新
对于上述动态更新流程,都是基于Flutter资源的完整替换,也即全量,而Flutter的代码产物是比较大的,通常来说有几M,所以Flutter动态化方案中肯定得支持差量的动态更新,一些二进制差量工具如bsdiff、xdelta等,通过对比获取差量Patch包,下发后合成,最终完成替换更新,这种方案是可行的