热修复技术现在已经很成熟了,至今还没有用过。虽然框架很多,但这里只介绍Sophix,原因不言而喻,对于技术来说谁的好用用谁的。Sophix亮点有一下几点
1,各大热修复框架对比图,详细对比请看Android 热修复调研报告—流行方案选择
2,Sophix的演化,阿里云官方文档
1,android studio集成方式
gradle远程仓库依赖, 打开项目找到app的build.gradle文件,添加如下配置:
添加maven仓库地址:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
2,添加gradle版本依赖:
compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.8'
3,添加权限
4,在AndroidManifest.xml中间的application节点下添加如下配置:
5,SDK接口接入
// initialize必须放在attachBaseContext最前面,初始化代码直接写在Application类里面,切勿封装到其他类。
SophixManager.getInstance().setContext(this)
.setAppVersion(appVersion)
.setAesKey(null)
.setEnableDebug(true)
.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) {
// 表明补丁加载成功
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
// 建议: 用户可以监听进入后台事件, 然后调用killProcessSafely自杀,以此加快应用补丁,详见1.3.2.3
} else {
// 其它错误信息, 查看PatchStatus类说明
}
}
}).initialize();
// queryAndLoadNewPatch不可放在attachBaseContext 中,否则无网络权限,建议放在后面任意时刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();
6,稳健接入
// 此处SophixEntry应指定真正的Application,并且保证RealApplicationStub类名不被混淆
@Keep
@SophixEntry(MyRealApplication.class)
static class RealApplicationStub {}
1,登录阿里云官网
2,创建应用
①,登录后进入管理控制台
②,添加产品,之后添加应用
③,应用添加完成
④,把appKey,AppSecret,RSA对应的值写入到AndroidManifest.xml中间的application节点下meta-data中,这里需要注意,如果你遇到这个问题 这就需要通过SDK接口接入的方式写入appKey,AppSecret,RSA。
通过setSecretMetaData(“App ID”,“App Secret”,“RSA密钥”)写入对应的值即可
1,生成补丁
修改项目xml中TextView内容,修改前打个包old.apk,修改后打个包new.apk。测试包不用签名,SDK初始化方法设置为setEnableDebug(true),然后下载补丁生成工具 补丁下载地址下载完成后运行SophixPatchTool.exe
2,本地测试方式
①,补丁生成后是一个jar,把这个jar拷贝到自己手机的skcard中,下载官方测试应用 测试程序
②,安卓6.0手机注意,需要动态添加权限。
添加动态权限
/**
* 如果本地补丁放在了外部存储卡中, 6.0以上需要申请读外部存储卡权限才能够使用. 应用内部存储则不受影响
*/
private void requestExternalStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_EXTERNAL_STORAGE_PERMISSION);
}
}
4,云端测试方式
用测试应用扫描二维码,扫描后会下载补丁,会有相应的日志
注意:如果正式发版,需要SDK初始化方法设置为setEnableDebug(false),生成的补丁需要签名,用自己应用的签名
5,异常定位
code异常定位,常见的一些code值。
//兼容老版本的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;//注入异常
6,SDK接口说明
①,initialize方法
initialize(): <必选>
该方法主要做些必要的初始化工作以及如果本地有补丁的话会加载补丁, 但不会自动请求补丁。因此需要自行调用queryAndLoadNewPatch方法拉取补丁。这个方法调用需要尽可能的早, 必须在Application的attachBaseContext方法的最前面调用(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后), initialize()方法调用之前你需要先调用如下几个方法进行一些必要的参数设置, 方法调用说明如下:
setContext(application): <必选> 传入入口Application即可
setAppVersion(appVersion): <必选> 应用的版本号
setSecretMetaData(idSecret, appSecret, rsaSecret): <可选,推荐使用> 三个Secret分别对应AndroidManifest里面的三个,可以不在AndroidManifest设置而是用此函数来设置Secret。放到代码里面进行设置可以自定义混淆代码,更加安全,此函数的设置会覆盖AndroidManifest里面的设置,如果对应的值设为null,默认会在使用AndroidManifest里面的。
setEnableDebug(isEnabled): <可选> isEnabled默认为false, 是否调试模式, 调试模式下会输出日志以及不进行补丁签名校验. 线下调试此参数可以设置为true, 查看日志过滤TAG:Sophix, 同时强制不对补丁进行签名校验, 所有就算补丁未签名或者签名失败也发现可以加载成功. 但是正式发布该参数必须为false, false会对补丁做签名校验, 否则就可能存在安全漏洞风险
setAesKey(aesKey): <可选> 用户自定义aes秘钥, 会对补丁包采用对称加密。这个参数值必须是16位数字或字母的组合,是和补丁工具设置里面AES Key保持完全一致, 补丁才能正确被解密进而加载。此时平台无感知这个秘钥, 所以不用担心阿里云移动平台会利用你们的补丁做一些非法的事情。
setPatchLoadStatusStub(new PatchLoadStatusListener()): <可选> 设置patch加载状态监听器, 该方法参数需要实现PatchLoadStatusListener接口, 接口说明见1.3.2.2说明
setUnsupportedModel(modelName, sdkVersionInt):<可选> 把不支持的设备加入黑名单,加入后不会进行热修复。modelName为该机型上Build.MODEL的值,这个值也可以通过adb shell getprop | grep ro.product.model取得。sdkVersionInt就是该机型的Android版本,也就是Build.VERSION.SDK_INT,若设为0,则对应该机型所有安卓版本。目前控制台也可以直接设置机型黑名单,更加灵活。
②,queryAndLoadNewPatch方法
该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地, 然后
应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.
应用已经存在一个补丁, 请求发现有新补丁后,本次不受影响。并且在下次启动时补丁文件删除, 下载并预加载新补丁。在下下次启动时应用新补丁。
补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.
只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.
同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号
③,killProcessSafely方法
可以在PatchLoadStatusListener监听到CODE_LOAD_RELAUNCH后在合适的时机,调用此方法杀死进程。注意,不可以直接Process.killProcess(Process.myPid())来杀进程,这样会扰乱Sophix的内部状态。因此如果需要杀死进程,建议使用这个方法,它在内部做一些适当处理后才杀死本进程。
④,cleanPatches()方法
清空本地补丁,并且不再拉取被清空的版本的补丁。正常情况下不需要开发者自己调用,因为Sophix内部会判断对补丁引发崩溃的情况进行自动清空。
⑤, PatchLoadStatusListener接口,
mode: 无实际意义, 为了兼容老版本, 默认始终为0
code: 补丁加载状态码, 详情查看PatchStatus类说明
info: 补丁加载详细说明
handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁
热修复方案
市面上流行的热修复框架主要有三个方案,类加载方案,底层替换方案和Instant Run方案
1,类加载方案
先了解一下Android的ClassLoader
Java Class的加载源码如下:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);//检查是否已经加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//委派给双亲
} else {
c = findBootstrapClassOrNull(name);//委派给启动类
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看出使用双亲委派机制,即先到父加载器进行加载,加载不到才使用自己进行加载。
类加载方案也是基于dex分包方案,由于Android项目有 65535方法限制,从而产生了dex分包方案。Dex分包是在打包时将代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex,从而缓解了主Dex的65536限制。
ClassLoader的加载过程中,会调用DexPathList中的findClass的方法代码如下:
public Class> findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element内部封装了DexFile用于加载dex文件,因此每个dex文件对应一个Element。这就是关键地方,我们可以将有bug的类test.class进行修改,然后将test.class打包dex的补丁包test.jar,放在Element数组dexElements的第一个元素,这样首先找到test.dex中的test.class去替换之前存在bug的test.class,排在数组后面的dex文件中的存在bug的test.class根据ClassLoader的双亲委托模式就不会被加载。
类加载方案需要重启App才能生效,不能即时生效。因为在App启动之后所有类已经加载完成,在Android上是无法对类进行卸载。如果不重启,类还在虚拟机中。
Sophix采用全量合成dex技术,直接利用Android原先的类查找和合成技术,快速合成新的全量dex。在虚拟机查找类的时候,会优先找到classes.dex中的类,然后才是classes2.dex,classes3.dex,也就是把补丁包中的类命名为classes.dex。
2,底层替换方案
底层替换不同的地方是可以及时生效,可以直接在Native层直接修改原类,底层替换方案通过在运行时利用hook操作native指针实现“热”的特性,底层替换所操作的指针,实际上是ArtMethod,在类被加载,类中的每个方法都会有对应的ArtMethod,它记录了方法包括所属类和内存地址信息。
由于不同的厂商对ArtMethod结构进行了修改,Sophix采用了对旧ArtMethod进行完整替换。因此Sophix采用类加载和底层替换相结合的方案。
3,Instant Run方案
Instant Run是基于多ClassLoader的,每一个patch都有一个ClassLoader,这就意味着如果你想更新patch,它都会创建一个ClassLoader,而在java中不同ClassLoader创建的类被认为是不同的,所以会重新加载新的patch中的补丁类。
Instant Run原理:
Sophix原理:
4,so修复
so库的修复本质上是对native方法修复和替换。
加载so的两个接口:
两种加载方式,实际上最后都调用nativeLoad这个native方法加载so库,这个方法参数filename:so库在磁盘中的完整路径名
Sophix是在启动期间反射注入patch中的so库,把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,从而达到替换的目的。
1,不支持四大组件
Sophix是不支持四大组件的修复,不支持四大组件(AndroidManifest中内容不能修改,组件内的代码可以修改)。如果要修复四大组件,必须在AndroidManifest里面预先插入代理组件,这样对app侵入性比较强。
2,即时生效的限制
即时生效只能支持方法的替换,而对于补丁类里面存在方法增加和减少,以及成员字段的增加和减少都不支持。
3,覆盖patch需要重启APP生效么?
4,不是所有资源都能修复
AndroidManifest.xml里面的变动无法修复,因为AndroidManifest.xml是由系统在安装app时解析,因此在运行时app无法修改它的逻辑的。
AndroidManifest.xml里面的资源不支持,通知栏图标、启动图标资源以及RemoteViews也不支持修复。原因是这类资源是由系统负责展示的,而系统只会在安装包中找资源,不会找到补丁包。
自测发现,资源中的音视频文件也无法实现修复,具体原因目前未知。
附加:如果是系统内置应用,要想使用Sophix必须要把Sophix生成的so文件拷贝到系统system/lib下。so查找方式,生成的apk修改后缀为zip,然后解压会有对应的so文件。有更多问题可以加入阿里钉钉群11711603