Flutter 热更新简介
所谓热更新,指的是当应用代码出现缺陷问题时,不需要重新打包提交App Store即可完成缺陷的修复。众所周知,使用原生技术开发的应用体验虽然好,但开发、上线周期长也常常被诟病,特别是当应用出现线上问题时,不得不重新打包发布,大大的影响了用户体验,而热更新技术就是为有效解决线上缺陷而提出的。
不过,热更新虽然具有很大的优点,但是滥用热修复也会给应用带来不好的体验,并且苹果对于热更新和修复是明令禁止的,所以热更新主要针对的是国内Android市场。目前,Flutter对外开放的SDK是不支持热更新的,但是在Flutter的源码里有一部分预埋的热更新相关的代码,可以通过一些必要的手段在Android端实现动态更新功能。
众所周知,不论是新创建的Flutter项目,还是原生工程以Moudle或者aar的方式集成Flutter,最终Flutter在原生Android端应用中都是以混合的形式存在的。所以,当我们拆开一个Flutter在release模式下编译生成的aar包时,其目录结构下图所示。
实际开发中,只需要关注assets、jni、libs这三个目录即可,其他都是原生的壳工程产物。
- jni:该文件目录下存放的是libflutter.so文件,该文件是Flutter引擎层的C++实现,提供skia绘制引擎、Dart和Text纹理绘制等支持。
- libs:该文件目录下存放的是flutter.jar文件,该文件为Flutter嵌入层的 Java实现,主要为Flutter的原生层提供平台功能支持,比如创建线程。
- assets:该文件目录主要用于存放Flutter应用层的资源,包括images、font等。
而众观目前所有的Flutter热更新方案中,其基本原理实现都是一样的,即通过修改libapp.so的加载路径,把它替换成开发者自己的libapp_hot.so来实现热更新。我们可以打开io.flutter.embedding.engine.loader包中的FlutterLoader类来查看libapp.so包的加载逻辑。
在原生Android开发中,我们可以使用Tinker、AndFix、Sophix和Robust等热修复框架来实现应用的热更新操作。综合比较,Tinker是一款不错的热修复框架,并且它支持修复so文件,既然Flutter热更新的基本原理是替换libapp.so,那么我们就可以利用Tinker热更新功能将需要修复的libapp_hot.so送达客户端,然后再加载libapp_hot.so文件即可实现代码的热更新。
接入Bugly
在使用Tinker之前,需要我们在原生Android平台接入Tinker。不过,在Flutter开发中,我们可以通过集成Bugly来集成Tinker,因为Bugly默认集成了Tinker,并且还提供缺陷上报和应用升级等功能,可谓一举两得。
接入Bugly之前,需要先开通Bugly账号,并在官网注册自己的产品,如果还没有账号,可以打开Bugly官网注册一个。使用Android Studio打开Flutter项目的Android工程,然后在根目录的build.gradle文件中添加如下脚本,如下所示。
buildscript {
dependencies {
// tinkersupport插件, 其中lastest.release表示拉取最新版本
classpath "com.tencent.bugly:tinker-support:1.2.0"
}
}
接着,打开Android工程app目录下的build.gradle文件,并在android节点和dependencies节点添加如下配置脚本。
android {
… //省略其他配置
defaultConfig {
ndk {
//设置支持的so库架构
abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.tencent.bugly:crashreport_upgrade:1.4.2'
//指定tinker依赖版本
implementation 'com.tencent.tinker:tinker-android-lib:1.9.14'
}
当需要更新升级SDK的时候,只需要更新配置脚本中的版本号即可。接下来,在build.gradle的同级目录下创建一个名为tinker-support.gradle的配置文件,并添加如下脚本。
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
//填写每次构建生成的基准包目录
def baseApkDir = "app-0208-15-10-00"
tinkerSupport {
//开启tinker-support插件,默认值true
enable = true
//指定归档目录,默认值当前module的子目录tinker
autoBackupApkDir = "${bakPath}"
//是否启用覆盖tinkerPatch配置功能,默认值false
overrideTinkerPatchConfiguration = true
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
//构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
tinkerId = "base-1.0.1"
enableProxyApplication = false
supportHotplugComponent = true
}
tinkerPatch {
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}
res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}
packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
buildConfig {
keepDexApply = false
}
}
在上面的配置脚本中,我们需要重点关注baseApkDir和tinkerId两个属性。其中,baseApkDir表示每次构建生成的基准包目录,而tinkerId表示构建基准包和补丁包需要指定的唯一标识。然后,再次打开build.gradle文件,在build.gradle文件的头部引入tinker-support.gradle配置脚本文件,如下所示。
//依赖Tinker插件脚本
apply from: 'tinker-support.gradle'
Bugly提供了两种初始化SDK的方式,区别是是否需要反射Application。首先,自定义一个继承自TinkerApplication的Application类,如下所示。
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "com.xzh.hotreload.SampleApplicationLike",
"com.tencent.tinker.loader.TinkerLoader", true);
}
}
可以发现,SampleApplication需要传入四个参数,需要变更的就是第二个参数,表示自定义的ApplicationLike,如下所示。
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onCreate() {
super.onCreate();
// SDK初始化,appId替换成你的在Bugly平台申请的appId, 调试时,将第三个参数改为true
Bugly.init(getApplication(), "900029763", false);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
// 安装tinker
// TinkerManager.installTinker(this); 替换成下面Bugly提供的方法
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
}
SampleApplicationLike类中Bugly初始化时需要用到appId,如果没有可以到Bugly官网注册应用,然后系统会生成一个对应的appId,如下图所示。
由于Tinker需要开启MultiDex,所以集成Bugly时还需要在build.gradle配置文件中添加multidex插件才可以使用MultiDex.install()方法。完成上述操作后,还需要在AndroidManifest.xml配置中添加如下配置。
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
//兼容Android N或者以上设备,必须配置FileProvider来访问共享路径的文件
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
需要说明的是,制作Android正式环境的基准包时,还需要在build.gradle文件中配置签名信息,如下所示。
android {
… //省略其他配置
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
当制作签名正式包时,为了避免因为混淆而造成代码功能异常,还需要在Proguard混淆文件中增加配置如下来避免混淆SDK。
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# tinker混淆规则
-dontwarn com.tencent.tinker.**
-keep class com.tencent.tinker.** { *; }
-keep class android.support.**{*;}
到此,原生Android接入Bugly就大体完成了。如果接入过程有任何问题,可以参考Bugly官网接入文档。
Flutter应用热更新
接下来,我们通过
执行Android的热更新操作之前,需要先制作应用的基准包。执行flutter build apk –release打包命令,或者打开Android Studio右边的Gradle面板执行打包操作,如下图所示。
执行assemble操作后,系统会在Flutter项目的build/app/bakApk文件夹下生成对应的基准包,并且每个正式签名的的基准包目录都会包含基准包、混淆配置文件和资源Id文件,如下图所示。
然后,将生成的app-release.apk正式基准包上传到Bugly的后台。当线上版本出现缺陷问题时,就可以使用tinker-support插件生成对应的补丁包。
修复代码里面的缺陷问题后,还需要修改tinker-support.gradle文件里面对应的baseApkDir和tinkerId的属性值,然后才能执行补丁包生成操作,如下图所示。
执行完补丁包生成操作之后,就会在Flutter工程的根目录的build/outputs文件夹下生成对应的补丁包文件,如下图所示。
然后,将生成的带签名的补丁包文件上传到Bugly后台,选择补丁的下发范围,点击立即下发让补丁生效,然后重新启动Flutter应用就可以看到应用更新后的效果。
注:
本文部分摘自《Flutter应用跨平台开发实战》(即将出版)
参考资料:
1,移动跨平台方案对比:WEEX、React Native、Flutter和PWA
2,Flutter入门与环境搭建
3,Flutter开发之Dart语言基础
4,Flutter基础知识
5,Flutter开发之基础Widgets
6,Flutter 应用程序调试
7,Flutter For Web入门实战
8,Flutter开发之异步编程
9,Flutter开发之网络请求
10,Flutter开发之JSON解析
11,Flutter开发之路由与导航
12,Flutter 必备开源项目
13,Flutter 国际化适配实战
14,Flutter应用集成极光推送
15,Flutter混合开发
16,构建属于自己的Flutter混合开发框架
17,Flutter 应用性能检测与优化
18,Flutter异常监测与上报
19,Flutter的Hot Reload是如何做到的
20,Apple为什么不封杀 Flutter,以后会封杀吗
21,《Flutter in action》开源