zhengxiaoyong

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引擎的支持:

1
2
3
4
5
6
7
8
9
10
11
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引擎即包含了它,构建脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
uri = new URI(metaData.getString("PatchServerURL") + "/" + getAPKVersion() + ".zip");

Config Patch Download Mode

Application节点下的MetaData属性,对应Key为PatchDownloadMode

1
String patchDownloadMode = metaData.getString("PatchDownloadMode");

Config Patch Install Mode

Application节点下的MetaData属性,对应Key为PatchInstallMode

1
String patchInstallMode = metaData.getString("PatchInstallMode");

Patch Installed Path

1
2
3
public File getInstalledPatch() {
return new File(context.getFilesDir().toString() + "/patch.zip");
}

Patch Downloaded Path

1
2
3
File getDownloadedPatch() {
return new File(getInstalledPatch().getPath() + ".install");
}

动态更新流程

Flutter Dynamic Patch全局是通过一个开关判断是否开启了Patch更新特性,即application中的meta-data属性:DynamicPatching

官方内部动态更新流程大致为:

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目录下重新生成时间戳文件,文件名称格式为:

1
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包,下发后合成,最终完成替换更新,这种方案是可行的

关注我

坚持原创技术分享,您的支持将鼓励我继续创作!