现状
安卓应用属于客户端程序,整个程序硬加载到用户设备中,所以安卓程序的升级在传统方法中是依赖用户的主动意识的。就意味着假如客户端出现了问题,用户是无法及时得到修复的。为了解决这个问题,便出现了很多热修复方案以及热更新框架,热更新相对原生升级策略具备很强的灵活性。这些技术同样适用于SDK。
SDK原生升级方案存在的问题
- 在传统升级策略中,SDK的升级严重依赖APP版本发布
- APP的更新依赖用户是否进行更新
- 无法及时对问题进行修复
目的
- 无缝衔接的热更新策略
- 实现客户端SDK的问题实时修复(一般指重新启动应用)
- java代码实时加载
- so实时加载
SDK热更新面临的问题
首次加载问题
- 如何处理外部加载的so和jar?
因为核心业务逻辑需要进行下发,实现热加载,那么在首次安装的时候,对于业务模块的加载需要进行特殊处理 - 如何处理so多架构加载问题?
因为OS在加载so文件时,会优先加载对应的CPU架构,其次才是兼容的cpu架构。对于热加载so,刚好绕过了这个问题
文件校验逻辑
- 如何对下发的文件进行安全加固和校验?
主要涉及到业务jar文件和so文件。一般会有一个版本号校验接口,文件下载接口。需要解决的问题:文件校验,防止文件被修改;文件加载预测试,判断文件是否被篡改
运行文件切换逻辑 running.jar、build-in.jar、download.jar
- 对于下发的文件,如何进行版本间的切换?
考虑存在多种情况:
下载版本 > 内置
下载版本 < 内置
需要控制真正应该加载的本地so和jar
安全性问题
如何对下载文件的信任度进行检查?
文件完整性校验、预加载校验
SDK热更新方案实施
接口文件
至少需要版本号、文件地址、校验码
文件拷贝
首次加载将sdk中的业务逻辑模块拷贝到应用私有目录中,类加载器只能加载应用私有目下的dex
加载文件
确定应该被加载的文件,版本号对比逻辑
下载替换
发现新版本下发文件时进行下载和本地替换
SDK项目拆分
- so拆分:原先的一个so拆分成common(主要负责基础功能,改动比较小)和business(主要负责业务功能,更新可能相对频繁)两个so
- jar拆分:原先的一个jar拆分成proxy.jar(主要是接口和调用核心业务逻辑的代码)和remote.jar两个jar,其中proxy是jar包,remote是dex包。理论上proxy包应该很小,可能只有一个接口类和一个加载dex文件的类
SDK加载时序
存储文件说明(仅以jar作说明)
- 内置build-in.jar、随SDK打包在assets中的jar
- 下载的download.jar、根据版本号检测下载到私有目录的jar
- 运行的running.jar、根据版本号比较,选择被加载的jar
首次打开(私有目录没有任何缓存)
- 拷贝assets中的dex(build-in.jar)到私有目录命名为running.jar
- 加载running.jar
- 将内置的jar版本号(联合应用版本)、running版本号存储到sp中
- 请求version接口,检查下发version是否大于内置version
- 下载下发文件,存储到私有目录download.jar,存储下发jar版本号到sp中
二次打开(私有目录中有缓存)
- 比较download.jar、running.jar、build-in.jar的版本
- download.jar > running.jar 或者build-in.jar > running.jar 则将download.jar/build-in.jar覆盖running.jar
之所以判断build-in.jar和running.jar的版本,是因为可能应用更新过,导致内置的jar版本号比缓存的高
加载dex代码片段
/**
* init(Application app, IEventConfig config)
* 调用初始化方法
* @param ctx 上下文
* @param config 配置标志
*/
public void execInit(Application ctx, IEventConfig config) {
if (!isRunningExists(ctx)) {
// 获取需要加载的dex,build-in || download || null
File loadDexFile = getToLoadDex(ctx);
if (loadDexFile != null) {
Log.i("wh", "running为空加载:" + loadDexFile.getAbsolutePath());
copyAndRenameRunningDex(ctx, loadDexFile);
SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
} else {
copyDexFromAssets(ctx);
// copy from assets && save assets dex version
Log.i("wh", "running为空加载内置");
copyAndRenameRunningDex(ctx, new File(getBuildInDexPath(ctx)));
SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
}
} else {
int runningVersion = SharePreferenceUtil.getInt("runningVersion");
int downloadVersion = SharePreferenceUtil.getInt("downloadVersion");
int internalRemoteDexVersion = ProxyConfig.getInternalRemoteDexVersion();
Log.i("wh", "内置版本号:" + internalRemoteDexVersion);
Log.i("wh", "运行版本号:" + runningVersion);
Log.i("wh", "下载版本号:" + downloadVersion);
if (internalRemoteDexVersion > runningVersion) {
if (internalRemoteDexVersion > downloadVersion) {
copyDexFromAssets(ctx);
// copy from assets && save assets dex version
Log.i("wh", "内置大于当前版本号,内置版本号最高,加载内置");
copyAndRenameRunningDex(ctx, new File(getBuildInDexPath(ctx)));
SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
} else {
Log.i("wh", "内置大于当前版本号,下载版本号大于内置,加载下载");
copyAndRenameRunningDex(ctx, new File(getDownloadDexPath(ctx)));
SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
}
} else {
if (downloadVersion > runningVersion) {
Log.i("wh", "内置小于等于当前版本号,下载版本号大于当前,加载下载");
copyAndRenameRunningDex(ctx, new File(getDownloadDexPath(ctx)));
SharePreferenceUtil.putInt("runningVersion", VersionUtil.getJarVersionFromManifest(getRunningDexPath(ctx)));
}
}
}
if (isRunningExists(ctx)) {
if (sInstance == null) {
File runningDex = new File(getRunningDexPath(ctx));
loadDexFromFile(ctx, runningDex);
}
try {
// 调用dex中的入口API
if (sInstance != null) {
Method method = sInstance.getMethod("init", Application.class, IEventConfig.class, String.class, String.class);
method.invoke(null, ctx, config);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
//加载完毕后,检查更新
checkUpdate();
}
方案实施的要点说明
- 壳jar的抽取、壳so的抽取
- 初始化时,正确jar的选择逻辑
- 对加载异常的控制
- 面向接口编程