前言
最近年底项目也没事做了,琢磨着研究一下热修复方案。市面上出现很多热修复方案,大致分为几种,一种是dex插桩的、一种Instant run、还有一种是通过底层做修改。本文主要研究阿里云最新的热修复方案Sophix,Sophix较其他的优劣势如下,对比发现Sophix热修复最大的特点就是即使生效(已有方法修改)和冷启动修复(新增方法和资源修改),并且Sophix已经做到兼容Android P,目前市场上比较成熟的主要是微信Tinker和阿里的Sophix,另一篇Android——Tinker热修复接入
创建项目
1.1 注册登录
阿里云官网->登陆账号->产品->企业应用->移动云下面的热修复,切换到当前页面先去注册登录,登录之后,如果是之前没用过,会显示立即开通,之前已经开通的话会直接显示管理控制台如下图所示:
1.2 开通产品
如果是第一次使用阿里云产品会先开通使用产品,开通之后如下图所示,点击加号开通产品,产品里面包含一系列,相当于一个产品集合,暂时只使用热修复,我开通的如下:
1.3 创建热修复项目
点击下图的加号即可创建项目,创建总分为三步,具体如下图:
第二步会产生一个json文件,是当前项目所有阿里云产品的配置,如果只做热修复,只需要关心热修复的三个主要参数,如下图所示的参数(appKey、hotfix.idSecret、hotfix.rsaSecret)。下面是配置信息,如果按照正常接入步骤可以按照文档接入,本文主要采用兼容Android 9.0稳健方式接入,下面的json配置文件尽量不要按照普通接入方式复制在项目下(除非是测试项目)。
"config": {
"emas.appKey":"27478432",
"emas.appSecret":"5e2f53a5aa6b8a48e74ac7146688b65c",
"emas.packageName":"com.example.testsophix",
"hotfix.idSecret":"2547832-1",
"hotfix.rsaSecret":"//////",
"httpdns.accountId":"102949",
"httpdns.secretKey":"826b1d8ad5119dd34f1a855eedd3cea0"
说明:
- 应用名称可以随意填写,建议填写项目中的appname,如果以后项目多避免混乱
- 包名一定要填写清单列表中包名
-
建议json配置不要放在项目中,如果忘记下载json配置也没关系,创建之后如下图所示点击下载配置即可获取配置文件
项目接入SDK
本文主要接入Sophix稳健方式的,兼容Android9.0,因为Android9.0对于部分热修复方式做简单修改,如果普通接入会出现奔溃或者修复不成功问题,下面是具体接入Android studio(3.0)方式,更多方式参考文档。
2.1 Project的build.gradle配置
Project项目下的build.gradle文件,添加maven仓库地址,添加如下配置:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
2.2 module级别build.gradle配置
module下build.gradle文件添加版本依赖:
compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.7'
2.3 权限配置
说明:
- 当使用调试工具(后面会提)调试补丁的时候,如果补丁放在本地,需要加读权限,并且Android 6.0以上的读写权限动态申请
- 联网权限是用于拉取阿里服务器补丁
2.4 配置SDK参数
配置参数有两种方式,一种是清单列表中,第二种是在项目的Application中通过参数的形式,推荐使用第二种(相对较为安全)
2.4.1 AndroidManifest文件配置SDK参数
其中三个参数对应上文提到的json文件里面的三个参数
2.4.2 代码中配置SDK参数
SophixManager.getInstance()
.setContext(this)
.setAppVersion(appVersion)
.setSecretMetaData(null, null, null)//这里填写参数
.setEnableDebug(true)
.setEnableFullLog()
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
Log.i(TAG, "sophix load patch success!");
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 如果需要在后台重启,建议此处用SharePreference保存状态。
Log.i(TAG, "sophix preload patch success. restart app to make effect.");
}
}
}).initialize();
- Sophix初始化的时候.setSecretMetaData(null, null, null)设置三个参数,顺序依次跟AndroidManifest一致。
2.5 混淆配置
#基线包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/build/outputs/mapping/release路径下,移动到/app路径下
#修复后的项目使用,保证混淆结果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{*;}
-keep class com.ta.utdid2.device.**{*;}
-dontwarn com.alibaba.sdk.android.utils.**
#防止inline
-dontoptimize
#看情况是否要混淆
-keepclassmembers class com.example.testsophix.MyRealApplication {
public ();
}
说明:如果项目混淆开启的情况下,在app/build/outputs/mapping/release路径下会生成mapping.txt,建议开启混淆(保证一些参数被混淆)。
2.6 SDK初始化
此稳健方式接入,使用了最新版本的Sophix,最新版本的Sophix采用了代理Application方式接入SDK,使自身的Application和Sophix初始化解耦合。初始化在继承SophixApplication的SophixStubApplication(名称可以自定义)中实现,其中自身的Application不需要做任何修改操作。
2.6.1 Application中初始化Sophix
public class SophixStubApplication extends SophixApplication {
private final String TAG = "SophixStubApplication";
private final String hotfixId = "25478432-1";
private final String hotfixappKey = "5e2f53a5aa6b8a48e74ac7146688b65c";
/**
* 此处SophixEntry应指定真正的Application,
* 并且保证RealApplicationStub类名不被混淆。
*
* @keep 注解已经做好了,无需在混淆文件做处理
*/
@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//如果需要使用MultiDex,需要在此处调用。
// MultiDex.install(this);
initSophix();
}
@Override
public void onCreate() {
super.onCreate();
//联网下载新的插件
SophixManager.getInstance().queryAndLoadNewPatch();
}
/**
* initialize最好放在attachBaseContext最前面
*/
private void initSophix() {
SophixManager.getInstance().setContext(this)
.setAppVersion(BuildConfig.VERSION_NAME)
.setAesKey(null)
.setSecretMetaData(hotfixId,hotfixappKey,getString(R.string.hotfixrsaSecret))
.setEnableDebug(true)
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
Log.e(TAG, "修复模式:" + mode);
Log.e(TAG, "修复回调code:" + code);
Log.e(TAG, "修复信息:" + info);
Log.e(TAG, "修复版本:" + handlePatchVersion);
// 补丁加载回调通知
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
// 表明补丁加载成功
Log.e(TAG, "表明补丁加载成功");
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
// 建议: 用户可以监听进入后台事件, 然后应用自杀
Log.e(TAG, "表明新补丁生效需要重启. 开发者可提示用户或者强制重启");
} else if (code == PatchStatus.CODE_LOAD_FAIL) {
// 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载
// SophixManager.getInstance().cleanPatches();
Log.e(TAG, "内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载");
} else {
// 其它错误信息, 查看PatchStatus类说明
Log.e(TAG, "其它错误信息, 查看PatchStatus类说明");
}
}
}).initialize();
}
}
2.6.2 修改AndroidManifest里面的Application名称
... ...
说明:
- 需要把AndroidManifest里面的Application名称改为这个新增的SophixStubApplication类(一定要修改,否则无法初始化Sophix)
- SophixStubApplication必须要继承SophixApplication
- @SophixEntry(MyRealApplication.class)处的MyRealApplication一定要填写自己本身的Application
- 这里的@Keep是android.support包中的类,目的是为了防止这个内部静态类的类名被混淆,因为sophix内部会反射获取这个类的,如果项目中没有依赖android.support的话,就需要在progurad里面手动指定RealApplicationStub不被混淆(就是自己的Application)。(详情参见文档)例子如下:
-keepclassmembers class com.my.pkg.MyRealApplication {
public ();
}
# 如果不使用android.support.annotation.Keep则需加上此行
# -keep class com.my.pkg.SophixStubApplication$RealApplicationStub
2.7 接口说明
2.7.1 initialize方法
- initialize(): <必选>
该方法主要做些必要的初始化工作以及如果本地有补丁的话会加载补丁, 但不会自动请求补丁。因此需要自行调用queryAndLoadNewPatch方法拉取补丁。这个方法调用需要尽可能的早, 必须在Application的attachBaseContext方法的最前面调用(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后), initialize()方法调用之前你需要先调用如下几个方法进行一些必要的参数设置, 方法调用说明如下: - setContext(application): <必选> 传入入口Application即可
- setAppVersion(appVersion): <必选> 应用的版本号
说明:appVersion在产生补丁的两包必须相同,否则无法加载补丁
appVersion默认取的module的build.gradle里面的versionName,当然可以自定义,推荐使用versionName,并且最好与热修复控制台的版本保持一致,防止版本多了混乱。 - setSecretMetaData(idSecret, appSecret, rsaSecret): <可选,推荐使用> 三个Secret分别对应AndroidManifest里面的三个,可以不在AndroidManifest设置而是用此函数来设置Secret。放到代码里面进行设置可以自定义混淆代码,更加安全,此函数的设置会覆盖AndroidManifest里面的设置,如果对应的值设为null,默认会在使用AndroidManifest里面的。
- setAesKey(aesKey): <可选> 用户自定义aes秘钥, 会对补丁包采用对称加密。这个参数值必须是16位数字或字母的组合,是和补丁工具设置里面AES Key保持完全一致, 补丁才能正确被解密进而加载。此时平台无感知这个秘钥, 所以不用担心阿里云移动平台会利用你们的补丁做一些非法的事情。
- setPatchLoadStatusStub(new PatchLoadStatusListener()): <可选,推荐> 设置patch加载状态监听器, 该方法参数需要实现PatchLoadStatusListener接口, 接口参数说明见2.7.2说明
2.7.2 PatchLoadStatusListener接口
该接口需要自行实现并传入initialize方法中, 补丁加载状态会回调给该接口, 参数说明如下:
- mode: 无实际意义, 为了兼容老版本, 默认始终为0
- code: 补丁加载状态码(code=1代表加载成功), 更多文档(超链接)PatchStatus类说明
- info: 补丁加载详细说明
- handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁
2.7.3 queryAndLoadNewPatch方法
该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地。
应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.
应用已经存在一个补丁, 请求发现有新补丁后,本次不受影响。并且在下次启动时补丁文件删除, 下载并预加载新补丁。在下下次启动时应用新补丁。
补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.
只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.
同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号
2.7.4 killProcessSafely方法
可以在PatchLoadStatusListener监听到CODE_LOAD_RELAUNCH后在合适的时机,调用此方法杀死进程。注意,不可以直接Process.killProcess(Process.myPid())来杀进程,这样会扰乱Sophix的内部状态。因此如果需要杀死进程,建议使用这个方法,它在内部做一些适当处理后才杀死本进程。
2.7.5 cleanPatches()方法
清空本地补丁,并且不再拉取被清空的版本的补丁。正常情况下不需要开发者自己调用,因为Sophix内部会判断对补丁引发崩溃的情况进行自动清空。
补丁管理
简单说一下Sophix修复的过程,首先需要两个apk包,一个是线上或者测试bug包,另一个是修复好问题的apk包,通过使用补丁工具,两个不同apk包会产生一个补丁,补丁上传阿里服务器,扫码下载补丁或者手动存放到本地,千万不要一开始就直接发布补丁,首先要用调试工具调试一下,查看是否完全符合自己的要求,当补丁符合要求再去发布,具体发布详情见下面的发布。并且要保证两个包是同一个版本,一个版本同时只能存在一个补丁,所以如果当前最近的补丁不是第一个补丁,那么最新的补丁一定是结合修复了之前所有补丁之后通过补丁工具生成的补丁。
3.1 创建补丁
3.1.1 下载补丁和调试工具
patch补丁包生成需要使用到打补丁工具SophixPatchTool, 如还未下载打包工具,请前往下载Android打包工具。
Mac版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_macos.zip
Windows版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_windows.zip
Linux版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_linux.zip
调试工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/hotfix_debug_tool-release.apk
该工具提供了Windows和macOS和Linux版本,Windows下运行SophixPatchTool.exe,macOS下运行SophixPatchTool.app,Linux下(Ubuntu 16.04 64bit最佳)运行SophixPatchTool。并且需要安装Java环境且在JDK7或以上才能正常使用。
说明:一些注意事项在补丁工具中会介绍(两张图片介绍)
3.1.2 补丁工具使用
解压补丁工具,运行SophixPatchTool.exe,如下图:
说明:
- 签名是必须要填写的,高级设置里面一些参数,具体情况而定
- 产生的补丁一定不要修改名称(补丁名称:sophix-patch.jar)
3.2 上传补丁
进入阿里云控制台,点击右侧的移动热修复,进入项目补丁管理
如上图在管理控制台,找到质量管理下面的移动热修复进入到补丁管理页面
3.2.1 添加补丁版本
此处添加的版本尽量与项目中的versionname保持一致
3.2.2 上传补丁
上传补丁里面描述,尽量要描述当前补丁修复那些内容,方便以后版本的回滚
3.2.3 发布调试补丁
点击发布之后,需要我们进行调试发布补丁,接下来进入调试、发布补丁阶段
调试补丁
4.1 调试工具使用
使用说明:
1、使用工具调试之前一定要先安装有bug的版本
2 、接下来连接应用
3、加载补丁有两种方式,一种是扫码二维码的方式,第二种是加载补丁放在本地,直接加载(本地加载需要读权限,涉及到Android6.0动态权限的设置),推荐使用扫描方式加载补丁。
4、通过下方的日志获取当前补丁的加载过程
4.2 加载补丁
扫描补丁之后查看调试工具日志输出,查看补丁情况,如果之前打补丁的时候高级设置里面设置强制冷启动,一定要先杀死当前测试热修复应用的进程,然后再次打开查看打补丁之后的情况。下面列举日志code码经常出现的几种情况更多code值请查看接口文档(超链接):
4.3 输出补丁日志
int CODE_LOAD_SUCCESS = 1;//加载阶段, 成功
int CODE_ERR_INBLACKLIST = 4;//加载阶段, 失败设备不支持
int CODE_REQ_NOUPDATE = 6;//查询阶段, 没有发布新补丁
int CODE_REQ_NOTNEWEST = 7;//查询阶段, 补丁不是最新的
int CODE_DOWNLOAD_SUCCESS = 9;//查询阶段, 补丁下载成功
int CODE_DOWNLOAD_BROKEN = 10;//查询阶段, 补丁文件损坏下载失败
int CODE_UNZIP_FAIL = 11;//查询阶段, 补丁解密失败
int CODE_LOAD_RELAUNCH = 12;//预加载阶段, 需要重启
int CODE_REQ_APPIDERR = 15;//查询阶段, appid异常
int CODE_REQ_SIGNERR = 16;//查询阶段, 签名异常
int CODE_REQ_UNAVAIABLE = 17;//查询阶段, 系统无效
int CODE_REQ_SYSTEMERR = 22;//查询阶段, 系统异常
int CODE_REQ_CLEARPATCH = 18;//查询阶段, 一键清除补丁
int CODE_PATCH_INVAILD = 20;//加载阶段, 补丁格式非法
4.4 补丁说明
说明一:patch是针对客户端具体某个版本的,patch和具体版本绑定
- 应用当前版本号是1.1.0, 那么只能在后台查询到1.1.0版本对应发布的补丁, 而查询不到之前1.0.0旧版本发布的补丁.
说明二:针对某个具体版本发布的新补丁, 必须包含所有的bugfix, 而不能依赖补丁递增修复的方式, 因为应用仅可能加载一个补丁
- 针对1.0.0版本在后台发布了一个补丁版本号为1的补丁修复了bug1, 然后发现此时针对这个版本补丁1修复的不完全, 代码还有bug2, 在后台重新发布一个补丁版本号为2的补丁, 那么此时补丁2就必须同时包含bug1和bug2的修复才行, 而不是只包含bug2的修复(bug1就没被修复了)
发布补丁
发布前请严格按照:扫码内测 => 灰度发布 => 全量发布的流程进行,以保证补丁包能够正常在所有Android版本的机型上生效。为了保险起见,理论上应该对每个版本的android手机都测一遍是否生效会比较好。不过,其实只需测试通过以下具有代表性的Android版本就基本没什么大问题了:4.0、4.4、5.1、7.0
注意事项
具体接入细节请参考官方文档:
非稳健方式接入
稳健方式接入