Flutter在Android上的热更新方案

1. 加载流程分析

flutter sdk版本:v3.0.3

dart版本:v2.17.5

1.1 FlutterApplication启动流程

按照安卓原生启动流程,当继承FlutterApplication且在AndroidManifest.xml将application:name设置后,除Content Provider初始化,Application流程将首先执行,此时FlutterApplication代码如下:

Flutter在Android上的热更新方案_第1张图片

FlutterApplication的onCreate只执行了FlutterLoader类下的startInitialzation,并把自己作为Context传入。

FlutterLoader对于startInitialzation有多个重载方法,此时将调用其中一个重载方法,并且生成了一个Settings传入。

Flutter在Android上的热更新方案_第2张图片

这时能看见startInitialization(@NonNull Context applicationContext, @NonNull Settings settings)才是实际执行的方法,方法前半段限制了该方法只能运行在主线程,如运行在非主线程则直接抛出异常。

新建了一个initTask的Callable类型的回调,并提交给了线程池执行。此时可以看见flutterJni.loadLibrary(),这里就是在加载libflutter.so,flutterJni该成员变量是FlutterInjector.instance()生成单例时,通过工厂+建造者生成的一个实例。

initResultFuture将作为线程任务执行的结果进行返回。

这说明了FlutterApplication执行过程中,最重要的就是在子线程中System.loadLibrary("flutter"),并且在线程执行完成后生成了initResultFuture,提供给后续操作。

Flutter在Android上的热更新方案_第3张图片

Flutter在Android上的热更新方案_第4张图片

1.2 FlutterActivity启动流程

如果已继承FlutterActivity作为LaunchActivity,且没有设置自定义的Application,将不会执行1.1的流程。

FlutterActivity时序图如下所示:

(下图出自得物技术专栏,链接:Flutter启动流程分析之插件化升级探索_configureflutterengine_得物技术的博客-CSDN博客)

Flutter在Android上的热更新方案_第5张图片

通过FlutterAcitivity的onCreate可以得知,大部分操作全部放在FlutterActivityAndFragmentDelegate中进行(代理模式)。

Flutter在Android上的热更新方案_第6张图片

deletegate.onAttach将自己注入到方法。此时由于成员变量flutterEngine必定为空,则执行setupFlutterEngine()。

Flutter在Android上的热更新方案_第7张图片

Flutter在Android上的热更新方案_第8张图片

进入setupFlutterEngine方法,此时可以通过getCachedEngineId,从FlutterEngineCache.getInstance()的map中,取出已经生成的flutterEngine实例,getCachedEngineId()方法具体实现在FlutterActivity。这里说明在FlutterActivity启动前预生成,可以有效减少onCreate()的时间。

当Cache和FlutterActivity子类中,都没有提供FlutterEngine时,将自己生成一个FlutterEngine,其参数将通过FlutterActivity子类提供。此时可以发现,通过FlutterActivity自动生成的Engine,automaticallyRegisterPlugins默认是为false的,这说明了此时需要手动注册插件,在预生成的Engine中可以设置为true。

Flutter在Android上的热更新方案_第9张图片

Flutter在Android上的热更新方案_第10张图片

通过FlutterEngine的构造方法,可以看见此时和FlutterApplication相似,生成了FlutterInjector的单例、flutterJni实例,并且初始化DartExecutor,生成了名为"flutter/isolate的channel,最后flutterJni与该executor的messager进行绑定。

此时由于flutterJni还未与Engine绑定,必定会执行FlutterLoader的startInitialzation方法,具体逻辑1.1中已经介绍。

之后将调用ensureInitialzationComplete的方法。

Flutter在Android上的热更新方案_第11张图片

该方法执行必定要在主线程中执行,且Settings不能为空。

此时可以发现,initResultFuture.get将挂起线程,并等待startInitialzation方法中执行的子线程结果后,才会继续向下执行。

这时定义了一个shellArgs,用于存放执行dart vm时携带的参数,并且可以将外部的args传入。

此时可以通过vm的参数,发现JIT和AOT将加载不同的产物:

共同加载:libflutter.so

JIT:vm_snapshot_data、isolate_snapshot_data、kernel_blob.bin

AOT:libapp.so、libvmservice_snapshot.so(Profile)

在AOT的代码段,注释:

// Most devices can load the AOT shared library based on the library name
// with no directory path. Provide a fully qualified path to the library
// as a workaround for devices where that fails.

这里说明了当第一段

"--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName 路径加载不到库时,将自动加载第二段绝对路径

"--"
+ AOT_SHARED_LIBRARY_NAME
+ "="
+ flutterApplicationInfo.nativeLibraryDir
+ File.separator
+ flutterApplicationInfo.aotSharedLibraryName

此时可以推断,"--" + AOT_SHARED_LIBRARY_NAME + PATH,当加载成功库,将不再执行同参数的后续加载。

最后将用flutterJni.init将参数传递给nativeInit的jni映射方法中,在jni层启动.

进入Jni层,可以通过flutter_main.cc文件下的Jni方法映射,找到实际引用的函数引用Init。

在Init方法中,将java层传输过来的args,增加了名叫flutter的参数

然后通过SettingsFromCommandLine生成了Settings类

Flutter在Android上的热更新方案_第12张图片

Flutter在Android上的热更新方案_第13张图片

Settings类中大部分都是作为设置的逻辑,在SettingsFromCommandLine方法中可以找到,aot_shared_library_name为数组,说明可以同时存在多个路径。

并且全部存到Settings类的application_library_path中。

通过注解:

// Path to a library containing the application's compiled Dart code.
// This is a vector so that the embedder can provide fallback paths in
// case the primary path to the library can not be loaded.

可以得知,该属性将作为默认加载路径无法加载后的备选路径。

通过dart vm的生命周期,dart_vm.cc文件中,Create方法会调用DartVMData的Create方法。

dart_vm_data.cc文件中的Create方法,将调用IsoSnapshotFromSettings函数,该函数中会调用SearchMapping函数。

SearchMapping函数中表明了会根据Path查找NativeLibrary,并且查找native_library_symbol_name符号名,如果查找到,则不会继续查找。此时可以证明只要Engine源码中,不改变该符号名的名称,则AOT产物替换加载方案为可行。

Flutter在Android上的热更新方案_第14张图片

Flutter在Android上的热更新方案_第15张图片

2. 热更新Android方案

根据加载流程分析来看,如果不修改jni代码,仅仅在java层进行改动,主要是在FlutterApplication与FlutterActivity上进行hook,将对于libapp.so的加载地址进行修改。

2.1 对于FlutterApplication的Hook

由1.1分析可以得出,FlutterApplication执行过程中,已经对于FlutterLoader进行了初始化,此时FlutterLoader的flutterInfoApplication属性已经填充了值,此时可以执行之后,使用Reflect替换掉flutterInfoApplication里的内容,将FlutterActivity加载库的地址指向新的文件地址即可。

方案1:

描述:继承FlutterApplication,修改flutterInfoApplication的aotSharedLibraryName属性

侵入性:中

结果:无效

原因:

1.原flutterApplicationInfo的nativeLibraryDir指向的是/data/app,只读不可写权限且属于私有文件目录,无法移动下载的补丁文件到nativeLibraryDir的路径下。

2.如将aotSharedLibraryName的绝对路径,通过../强制转成补丁文件目录,会触发jni路径检查错误。

错误信息如下:

library "/storage/emulated/0/Android/data/packagename/files/Download/-712256458372815974.so" ("/storage/emulated/0/Android/data/packagename/files/Download/-712256458372815974.so") needed or dlopened by "/data/app/~~F_tPgp6PDbt9Ug0opdFRsg==/packagename-W65G60j2V9GbWfwGQd_upw==/lib/arm64/libflutter.so" is not accessible for the namespace: [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/~~F_tPgp6PDbt9Ug0opdFRsg==/packagename-W65G60j2V9GbWfwGQd_upw==/lib/arm64:/data/app/~~F_tPgp6PDbt9Ug0opdFRsg==/packagename-W65G60j2V9GbWfwGQd_upw==/base.apk!/lib/arm64-v8a", permitted_paths="/data:/mnt/expand:/data/data/packagename"]

从Log中可以看见libflutter.so文件对于加载的路径,除了default_library_paths的,还有/data:/mnt/expand:/data/data/packagename 这2个路径。

Flutter在Android上的热更新方案_第16张图片

Flutter在Android上的热更新方案_第17张图片

方案2:

描述:继承FlutterApplication,修改flutterInfoApplication的aotSharedLibraryName、nativeLibraryDir属性

侵入性:中

结果:无效

原因:原flutterApplicationInfo的nativeLibraryDir指向的是/data/app,只读不可写权限且属于私有文件目录,如将libflutter.so转移到公共目录,成本过高。且公共目录会出现jni路径检查错误。具体原因可见方案1。

2.2 对于FlutterActivity的Hook

由1.2分析可以得出,即使onCreate执行后,也不能完全保证flutterApplicationInfo的属性被填充,必须等到子线程完成后才会注入数据,所以不可以通过2.1的操作,对于flutterApplicationInfo通过反射注入自定义路径。

通过1.2分析startInitialzationComplete方法,可以得出,FlutterActivity的args参数,可以被传入到方法中,与方法内的参数进行合并,并传入到jni层。并且此时注入到args的路径,必定顺序在源代码定义lib加载路径之前,由此可以得出自定义路径可以替代原路径进行加载。

方案1:

描述:继承FlutterActivity,重写getFlutterShellArgs方法,使得vm args中加入一条指向新文件的路径

侵入性:低

结果:有效

原因:通过1.2分析表明,jni层在根据路径查找so文件时,如果查找到文件,且符号名正确,将不会继续查找,所以修改getFlutterShellArgs传入自定义的文件路径,可以完成热更新的操作。

注意:此时当下载到新文件时,必须将新文件从公共路径(转移/写入)到/data/user/0/packageName路径以下,由上面link的日志可以看出,/data/data路径是可允许的加载路径,而/data/data是/data/user/0的软连接,且data/user/0/packageName路径下是拥有可读可写权限的。

Flutter在Android上的热更新方案_第18张图片

3.补丁下发方案设计

3.1补丁检查

方案1:

在登录账号时触发检测,通过登录时的接口来判定当时是否存在最新的补丁。

优点:结构简单

缺点:如app不关闭且不重新登录账号,则无法触发更新获取,时效性差。服务器无法保证即时通知到app进行更新。

流程图如下:

Flutter在Android上的热更新方案_第19张图片

方案2:

在登录账号时建立长链接,通过服务器推送来判定是否存在补丁。

优点:即时性高,服务器可以确保补丁下发过程中状态是否正常。

缺点:结构复杂,依赖于长链接的是否稳定。

流程图如下:

Flutter在Android上的热更新方案_第20张图片

3.2 版本管理

用户将上传自身的包版本、补丁包版本和包名。

补丁下发将存在2种模式:

1.全量下发

上传补丁后,将选择要发布的版本号,然后将上传补丁的分发至选择的版本号,如该版本用户本地补丁版本低于分发的补丁号,则将使用下发的补丁。

2.指定用户下发

上传补丁后,将针对指定用户下发补丁。如该版本用户本地补丁版本低于分发的补丁号,则将使用下发的补丁。

你可能感兴趣的:(热更新,flutter,android,ios)