移动热修复(Mobile Hotfix)是阿里云提供的全平台App热修复服务方案。产品基于阿里巴巴首创hotpatch技术,提供最细粒度热修复能力,让您无需等待实时修复应用线上问题。
主要解决问题
- 产品已有功能,出现问题,无需发版,即可发补丁,实时修复。
- 存量功能微调
HotFix总体来说最大的优势在于:
- 补丁即时生效,不需要应用重启;
- 补丁包同样采用差量技术,生成的PATCH体积小;
- 对应用无侵入,几乎无性能损耗;
- 两行代码,傻瓜式接入。
使用限制
- 支持加固,但是如果app中用到了加固,切记需要在加固前打补丁包。因为加固做了很多特殊处理,所以接入之后,尽量对加固的包,进行全面的测试。
- 目前阿里云的聚安全加固进行了兼容支持,经测试,爱加密、梆梆、360、乐固、娜迦加固下全版本均测试正常。
- 混淆和加固:混淆和加固是不一样的,加固可能包含了混淆,并且做了很多特殊处理。注:如出现未加固时修复正常,加固后修复时找不到方法或类的情况,查看未加固的包是否混淆,可先对项目进行常规混淆,混淆后的包能正常修复后再进行加固测试。
- 当前仅支持Android平台
- 当前热修复,支持存量功能错误问题的修复,或者存量功能类的调整,不支持增量新增功能。
- 注意:平台计费是基于一个计费接口(queryAndLoadNewPatch),不是发了补丁才会有计费。您可以根据自身需要,针对该计费接口,加上开关。
- 注意:平台计费,接口查询,是为了防止恶意查询,导致服务成本极速上升。给定了比较高的额度,是日均查询次数:20次。每个账号下,平均到每台设备,一天免费查补丁询20次。【这里是平均数】,超出部分是:2元/万次请求。
- 支持的资源:res资源 和 asset资源。
- Sophix 默认支持补丁大小30M以内
创建项目
1.1 注册登录
阿里云官网->登陆账号->产品->企业应用->移动云下面的热修复,切换到当前页面先去注册登录,登录之后,如果是之前没用过,会显示立即开通.
1.2 开通产品并创建热修复项目
如果是第一次使用阿里云产品会先开通使用产品,开通之后如下图所示,点击加号开通产品,产品里面包含一系列,相当于一个产品集合,暂时只使用热修复
第二步会产生一个json文件,是当前项目所有阿里云产品的配置,如果只做热修复,只需要关心热修复的三个主要参数,如下图所示的参数(appKey、hotfix.idSecret、hotfix.rsaSecret)。下面是配置信息,如果按照正常接入步骤可以按照文档接入,本文主要采用兼容Android 9.0稳健方式接入,下面的json配置文件尽量不要按照普通接入方式复制在项目下。
json文件中的一部分,包括项目要用到的appKey、appSecret、rsaSecret
{
"config": {
"emas.appKey":"29486055",
"emas.appSecret":"394a882eef83f5d3af3a14a1572cde86",
"emas.packageName":"com.example.alihot",
"hotfix.idSecret":"29486055-1",
"hotfix.rsaSecret":"MIIEvQIBADANBgkqhkiG9w0BqT9ALSe+vg=",
"httpdns.accountId":"193836",
"httpdns.secretKey":"420909cd9d988e938dfc8fadd146fe37",
"appmonitor.tlog.rsaSecret":"MIGfMA0GnROqZIrLhdbIsvLEgmzB"
}
项目接入SDK
本文主要接入Sophix稳健方式的,兼容Android9.0,因为Android9.0对于部分热修复方式做简单修改,如果普通接入会出现奔溃或者修复不成功问题,下面是具体接入Android studio(3.0)方式,更多方式参考文档。
1、Project项目下的build.gradle文件,添加maven仓库地址,添加如下配置:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
2、module级别build.gradle配置
implementation 'com.aliyun.ams:alicloud-android-hotfix:3.2.14'
注意,若SDK集成过程中出现UTDID冲突(一般是之前集成过支付宝),请参考:阿里云-移动云产品SDK UTDID冲突解决方案。
如若仓库访问失败, 那么用本地依赖的方式进行依赖, SDK下载见“1.5 客户端本地SDK及DEMO下载”节。
注意:使用android studio打包生成apk时,要关闭instant run。
3、权限配置
<! -- 网络权限 -->
<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_EXTERNAL_STORAGE"/>
READ_EXTERNAL_STORAGE权限属于Dangerous Permissions,仅调试工具获取外部补丁需要,不影响线上发布的补丁加载,调试时请自行做好android6.0以上的运行时权限获取。
4、AndroidManifest文件配置SDK参数
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密钥" />
注:App ID/App Secret将被用于计量计费,请妥善保管注意安全。
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.hotfixtext.MyApplication {
public <init>();
}
SDK接口使用说明
1、接入范例
initialize的调用应该尽可能的早,必须在Application.attachBaseContext()的最开始(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后)进行SDK初始化操作,初始化之前不能用到其他自定义类,否则极有可能导致崩溃。而查询服务器是否有可用补丁的操作可以在后面的任意地方。不建议在Application.onCreate()中初始化,因为如果带有ContentProvider,就会使得Sophix初始化时机太迟从而引发问题。
SDK稳健接入
Sophix最新版本引入了新的初始化方式。
原来的初始化方式仍然可以使用。只是新方式可以提供更全面的功能修复支持,将会带来以下优点:初始化与应用原先业务代码完全隔离,使得原先真正的Application可以修复,并且减少了补丁预加载时间等等。另外,新方式能够更完美地兼容Android 8.0以后版本。
具体而言,是需要用户自行加入以下这个类:
package com.my.pkg;
import android.app.Application;
import android.content.Context;
import android.support.annotation.Keep;
import android.util.Log;
import com.taobao.sophix.PatchStatus;
import com.taobao.sophix.SophixApplication;
import com.taobao.sophix.SophixEntry;
import com.taobao.sophix.SophixManager;
import com.taobao.sophix.listener.PatchLoadStatusListener;
import com.my.pkg.MyRealApplication;
/**
* Sophix入口类,专门用于初始化Sophix,不应包含任何业务逻辑。
* 此类必须继承自SophixApplication,onCreate方法不需要实现。
* 此类不应与项目中的其他类有任何互相调用的逻辑,必须完全做到隔离。
* AndroidManifest中设置application为此类,而SophixEntry中设为原先Application类。
* 注意原先Application里不需要再重复初始化Sophix,并且需要避免混淆原先Application类。
* 如有其它自定义改造,请咨询官方后妥善处理。
*/
public class SophixStubApplication extends SophixApplication {
private final String TAG = "SophixStubApplication";
// 此处SophixEntry应指定真正的Application,并且保证RealApplicationStub类名不被混淆。
@Keep
@SophixEntry(MyRealApplication.class) //MyRealApplication-->修改成自己的全局类
static class RealApplicationStub {}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 如果需要使用MultiDex,需要在此处调用。
// MultiDex.install(this);
initSophix();
}
private void initSophix() {
String appVersion = "0.0.0";
try {
appVersion = this.getPackageManager()
.getPackageInfo(this.getPackageName(), 0)
.versionName;
} catch (Exception e) {
}
final SophixManager instance = SophixManager.getInstance();
instance.setContext(this)
.setAppVersion(appVersion)
.setSecretMetaData(key, Secret, rsaSecret)//填写申请的key
.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();
}
}
// queryAndLoadNewPatch不可放在attachBaseContext 中,否则无网络权限,建议放在后面任意时刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();
@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {}
SophixEntry应指定项目中原先真正的Application(原项目里application的android::name指定的),这里用MyRealApplication指代。并且保证RealApplicationStub类名不被混淆。而SophixStubApplication的类名和包名可以自行取名。
这里的Keep是android.support包中的类,目的是为了防止这个内部静态类的类名被混淆,因为sophix内部会反射获取这个类的SophixEntry。如果项目中没有依赖android.support的话,就需要在progurad里面手动指定RealApplicationStub不被混淆,详见下文。
然后,在proguard文件里面需要加上下面内容:
-keepclassmembers class com.example.test.MyApplication {
public <init>();
}
# 如果不使用android.support.annotation.Keep则需加上此行
# -keep class com.my.pkg.SophixStubApplication$RealApplicationStub
目的是防止真正Application的构造方法被proguard混淆。
最后,需要把AndroidManifest里面的application改为这个新增的SophixStubApplication类:
<application
android:name="com.my.pkg.SophixStubApplication"
... ...>
... ...
这样便完成了新方式的初始化接入改造。
总结一下,过程一共有四个步骤:
- 把此SophixStubApplication入口类添加进项目中,所有Sophix相关初始化放在此类中。并且不应包含开发者的任何业务逻辑代码。 若使用了MultiDex,也应在SophixStubApplication的initSophix之前添加,并且需要记得在原来的Application里面去除MultiDex,避免重复调用导致问题。
- 把RealApplicationStub的SophixEntry注解的内容改为自己原先真正的MyRealApplication类。
- 混淆文件中确保某些内容不被混淆。
- AndroidManifest里面的application改为新增的SophixStubApplication入口类。
2、接口说明
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 PatchLoadStatusListener接口
该接口需要自行实现并传入initialize方法中, 补丁加载状态会回调给该接口, 参数说明如下
- mode: 无实际意义, 为了兼容老版本, 默认始终为0
- code: 补丁加载状态码(code=1代表加载成功), 更多文档(超链接)PatchStatus类说明
- info: 补丁加载详细说明
- handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁
3 queryAndLoadNewPatch方法
该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地。
- 应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.
- 应用已经存在一个补丁, 请求发现有新补丁后,本次不受影响。并且在下次启动时补丁文件删除, 下载并预加载新补丁。在下下次启动时应用新补丁。
- 补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.
只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.
同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号
4 killProcessSafely方法
可以在PatchLoadStatusListener监听到CODE_LOAD_RELAUNCH后在合适的时机,调用此方法杀死进程。注意,不可以直接Process.killProcess(Process.myPid())来杀进程,这样会扰乱Sophix的内部状态。因此如果需要杀死进程,建议使用这个方法,它在内部做一些适当处理后才杀死本进程。
5 cleanPatches()方法
清空本地补丁,并且不再拉取被清空的版本的补丁。正常情况下不需要开发者自己调用,因为Sophix内部会判断对补丁引发崩溃的情况进行自动清空。
常见状态码说明如下: 一个补丁的加载一般分为三个阶段: 查询/预加载/加载
//兼容老版本的code说明
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;//加载阶段, 补丁格式非法
//查询阶段的code说明
int CODE_QUERY_UNDEFINED = 31;//未定义异常
int CODE_QUERY_CONNECT = 32;//连接异常
int CODE_QUERY_STREAM = 33;//流异常
int CODE_QUERY_EMPTY = 34;//请求空异常
int CODE_QUERY_BROKEN = 35;//请求完整性校验失败异常
int CODE_QUERY_PARSE = 36;//请求解析异常
int CODE_QUERY_LACK = 37;//请求缺少必要参数异常
//预加载阶段的code说明
int CODE_PRELOAD_SUCCESS = 100;//预加载成功
int CODE_PRELOAD_UNDEFINED = 101;//未定义异常
int CODE_PRELOAD_HANDLE_DEX = 102;//dex加载异常
int CODE_PRELOAD_NOT_ZIP_FORMAT = 103;//基线dex非zip格式异常
int CODE_PRELOAD_REMOVE_BASEDEX = 105;//基线dex处理异常
//加载阶段的code说明 分三部分dex加载, resource加载, lib加载
//dex加载
int CODE_LOAD_UNDEFINED = 71;//未定义异常
int CODE_LOAD_AES_DECRYPT = 72;//aes对称解密异常
int CODE_LOAD_MFITEM = 73;//补丁SOPHIX.MF文件解析异常
int CODE_LOAD_COPY_FILE = 74;//补丁拷贝异常
int CODE_LOAD_SIGNATURE = 75;//补丁签名校验异常
int CODE_LOAD_SOPHIX_VERSION = 76;//补丁和补丁工具版本不一致异常
int CODE_LOAD_NOT_ZIP_FORMAT = 77;//补丁zip解析异常
int CODE_LOAD_DELETE_OPT = 80;//删除无效odex文件异常
int CODE_LOAD_HANDLE_DEX = 81;//加载dex异常
// 反射调用异常
int CODE_LOAD_FIND_CLASS = 82;
int CODE_LOAD_FIND_CONSTRUCTOR = 83;
int CODE_LOAD_FIND_METHOD = 84;
int CODE_LOAD_FIND_FIELD = 85;
int CODE_LOAD_ILLEGAL_ACCESS = 86;
//resource加载
public static final int CODE_LOAD_RES_ADDASSERTPATH = 123;//新增资源补丁包异常
//lib加载
int CODE_LOAD_LIB_UNDEFINED = 131;//未定义异常
int CODE_LOAD_LIB_CPUABIS = 132;//获取primaryCpuAbis异常
int CODE_LOAD_LIB_JSON = 133;//json格式异常
int CODE_LOAD_LIB_LOST = 134;//lib库不完整异常
int CODE_LOAD_LIB_UNZIP = 135;//解压异常
int CODE_LOAD_LIB_INJECT = 136;//注入异常
补丁管理
简单说一下Sophix修复的过程,首先需要两个apk包,一个是线上或者测试bug包,另一个是修复好问题的apk包,通过使用补丁工具,两个不同apk包会产生一个补丁,补丁上传阿里服务器,扫码下载补丁或者手动存放到本地,千万不要一开始就直接发布补丁,首先要用调试工具调试一下,查看是否完全符合自己的要求,当补丁符合要求再去发布,具体发布详情见下面的发布。并且要保证两个包是同一个版本,一个版本同时只能存在一个补丁,所以如果当前最近的补丁不是第一个补丁,那么最新的补丁一定是结合修复了之前所有补丁之后通过补丁工具生成的补丁。
3、 创建补丁
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或以上才能正常使用。
说明:一些注意事项在补丁工具中会介绍(两张图片介绍)
2 补丁工具使用
解压补丁工具,运行SophixPatchTool.exe,如下图:
说明:
- 签名是必须要填写的,高级设置里面一些参数,具体情况而定
- 产生的补丁一定不要修改名称(补丁名称:sophix-patch.jar)
- 注意,补丁包的名字必须是 sophix-patch.jar ,不能修改!
3上传补丁
使用说明:
- 使用工具调试之前一定要先安装有bug的版本
- 接下来连接应用
- 加载补丁有两种方式,一种是扫码二维码的方式,第二种是加载补丁放在本地,直接加载(本地加载需要读权限,涉及到Android6.0动态权限的设置),推荐使用扫描方式加载补丁。
- 通过下方的日志获取当前补丁的加载过程
扫描补丁之后查看调试工具日志输出,查看补丁情况,如果之前打补丁的时候高级设置里面设置强制冷启动,一定要先杀死当前测试热修复应用的进程,然后再次打开查看打补丁之后的情况。
灰度发布:可指定修复补丁的手机数量
全量发布:用于生产环境。经本地测试,灰度发布测试没问题后,就可以全量发布了
发布完毕,杀掉进程(而不是返回)才会生效。
发布补丁
发布前请严格按照:扫码内测 => 灰度发布 => 全量发布的流程进行,以保证补丁包能够正常在所有Android版本的机型上生效。为了保险起见,理论上应该对每个版本的android手机都测一遍是否生效会比较好。不过,其实只需测试通过以下具有代表性的Android版本就基本没什么大问题了:4.0、4.4、5.1、7.0
官网demo地址:[https://github.com/aliyun/alicloud-android-demo/tree/master/hotfix_android_demo]
转载:https://blog.csdn.net/weixin_43976155/article/details/123484328