一 热修复
通常解决bug都是通过发布版本的形式达到的,在下一个版本发布之前,用户是用不出现bug之后的功能,给用户体验非常差, 而热修复可以避免频繁发布版本。
热修复技术指的是应用不需要发版,用户无需安装就可以更新应用的部分内容,主要用于线上版本的bug快速、无感修复
本文主要简单介绍热修复的一种 :andfix
1 特点
- 随时随地可以在后台修复bug
- 减少公司在营运中的损失。因为一旦产品发布,出现奔溃的现象后,用户是用不了app的功能的
- 避免频繁发布版本
- 一次修复 只要手机不关机的情况,再无需修复
- 可以在线更改某些功能
2 主流的热修复技术
- thinker :微信
- andfix:淘宝,性能优于thinker,但是太依赖底层
- sophix(未开源):主要解决size的问题然后利用memcpy替换Method
3 修复步骤
- 发现bug 并修改bug,将修复的java文件 编译成class 然后打包成dex 放到服务器 供客户端下载
- 将修复的方法体 Method 从dex 文件取出,将会出现bug的方法 Method 也取出来
- 将取出的正确的 和 错误的method 一并传到底层做替换操作
- 底层替换 ( 指针替换)
4 CLASS_ISPREVERIFIED问题
为什么微信热修复需要重启
-
在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
-
校验方式:如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
A类 在开始分包的时候就引用了其他的dex java类 CLASS_ISPREVERIFIED标记
-
被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
-
而普通分包方案则不会出现这个错误,
因为引用和被引用的两个类一开始就不在同一个dex中
所以校验的时候并不会被打上CLASS_ISPREVERIFIED
怎么样避免打CLASS_ISPREVERIFIED标记?
解决: JNI实现新旧METHOD替换的同时,修改CLASS_ISPREVERIFIED标记
二 JAVA层逻辑
ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,
Android 4.4默认采用的还是DVM,
系统会提供一个选项来开启ART。
在Android 5.0时,默认采用ART,DVM从此退出历史舞台。
public class DexManager {
private Context context;
private static final DexManager ourInstance = new DexManager();
public static DexManager getInstance() {
return ourInstance;
}
private DexManager() {
}
public void setContext(Context context) {
this.context = context;
}
public void loadFile(File file) {
try {
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
//下一步 得到class ----取出修复好的Method
Enumeration<String> entry= dexFile.entries();
while (entry.hasMoreElements()) {
// 拿到全类名
String className=entry.nextElement();
Class clazz=dexFile.loadClass(className, context.getClassLoader());
if (clazz != null) {
fixClazz(clazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void fixClazz(Class realClazz) {
// 服务器修复好的 realClazz
Method[] methods=realClazz.getDeclaredMethods();
for (Method rightMethod : methods) {
Replace replace = rightMethod.getAnnotation(Replace.class);
if (replace == null) {
continue;
}
//找到了修复好的Method 找到出bug的Method
String wrongClazz=replace.clazz();
String wrongMethodName=replace.method();
try {
Class clazz=Class.forName(wrongClazz);
Method wrongMethod = clazz.getDeclaredMethod(wrongMethodName, rightMethod.getParameterTypes());
if (Build.VERSION.SDK_INT <= 18) {
replace(Build.VERSION.SDK_INT ,wrongMethod, rightMethod);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
public native void replace(int sdk,Method wrongMethod, Method rightMethod);
}
因为在JNI调用修复的方法中要使用到相应的系统API,所以要引用对应虚拟机系统的头文件
三 Dalvik 修复
-
引入Dalvik 系统的 dalvik.h头文件
-
实现native修复方法:Java_com_example_ray_fix_DexManager_replace
#include <jni.h>
#include <string>
#include "dalvik.h"
typedef Object *(*FindObject)(void *thread, jobject jobject1);
typedef void* (*FindThread)();
FindObject findObject;
FindThread findThread;
extern "C"{
JNIEXPORT void JNICALL
Java_com_example_ray_fix_DexManager_replace(JNIEnv *env, jobject instance, jint sdk,jobject wrongMethod,
jobject rightMethod) {
// 做 跟什么有关 虚拟机 java虚拟机 Method
//找到虚拟机对应的Method 结构体
Method *wrong = (Method *) env->FromReflectedMethod(wrongMethod);
Method *right =(Method *) env->FromReflectedMethod(rightMethod);
//下一步 把right 对应Object 第一个成员变量ClassObject status
// ClassObject
void *dvm_hand=dlopen("libdvm.so", RTLD_NOW);
// sdk 10 以前是这样 10会发生变化
findObject= (FindObject) dlsym(dvm_hand, sdk > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
findThread = (FindThread) dlsym(dvm_hand, sdk > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
// method 所声明的Class
jclass methodClaz = env->FindClass("java/lang/reflect/Method");
jmethodID rightMethodId = env->GetMethodID(methodClaz, "getDeclaringClass",
"()Ljava/lang/Class;");
//
jobject ndkObject = env->CallObjectMethod(rightMethod, rightMethodId);
ClassObject *firstFiled = (ClassObject *) findObject(findThread(), ndkObject);
firstFiled->status=CLASS_INITIALIZED;
wrong->accessFlags |= ACC_PUBLIC;
wrong->methodIndex=right->methodIndex;
wrong->jniArgInfo=right->jniArgInfo;
wrong->registersSize=right->registersSize;
wrong->outsSize=right->outsSize;
// 方法参数 原型
wrong->prototype=right->prototype;
//
wrong->insns=right->insns;
wrong->nativeFunc=right->nativeFunc;
}
}
四 ART修复
引入Art虚拟器中的相关源码,如7.0的:art_7_0.h
#include <jni.h>
#include <string>
#include "dalvik.h"
typedef Object *(*FindObject)(void *thread, jobject jobject1);
typedef void* (*FindThread)();
FindObject findObject;
FindThread findThread;
extern "C"{
JNIEXPORT void JNICALL
Java_com_example_ray_fix_DexManager_replace(JNIEnv *env, jobject instance, jint sdk,jobject wrongMethod,
jobject rightMethod) {
// 做 跟什么有关 虚拟机 java虚拟机 Method
//找到虚拟机对应的Method 结构体
Method *wrong = (Method *) env->FromReflectedMethod(wrongMethod);
Method *right =(Method *) env->FromReflectedMethod(rightMethod);
//下一步 把right 对应Object 第一个成员变量ClassObject status
// ClassObject
void *dvm_hand=dlopen("libdvm.so", RTLD_NOW);
// sdk 10 以前是这样 10会发生变化
findObject= (FindObject) dlsym(dvm_hand, sdk > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
findThread = (FindThread) dlsym(dvm_hand, sdk > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
// method 所声明的Class
jclass methodClaz = env->FindClass("java/lang/reflect/Method");
jmethodID rightMethodId = env->GetMethodID(methodClaz, "getDeclaringClass",
"()Ljava/lang/Class;");
//
jobject ndkObject = env->CallObjectMethod(rightMethod, rightMethodId);
ClassObject *firstFiled = (ClassObject *) findObject(findThread(), ndkObject);
firstFiled->status=CLASS_INITIALIZED;
wrong->accessFlags |= ACC_PUBLIC;
wrong->methodIndex=right->methodIndex;
wrong->jniArgInfo=right->jniArgInfo;
wrong->registersSize=right->registersSize;
wrong->outsSize=right->outsSize;
// 方法参数 原型
wrong->prototype=right->prototype;
//
wrong->insns=right->insns;
wrong->nativeFunc=right->nativeFunc;
}
}
五 解决混淆产生的问题
使用工具:apkpatch 将新旧apk进行对比,会将修改的类,打包生成dex文件
注意:由于工具类中将相关的包名路径写死了,所以修复版Replace要重命名为MethodReplace,并修改包路径为:
com.alipay.euler.andfix.annotation
命令
apkpatch.bat -f new.apk -t old.apk -o output -k andfix.jk -p 123456 -a david -e 123456
-f 修复好的apk
-t 出现bug的apk
-o 输出的文件
-k 签名
-p 签名 密码
-a 别名
-e 别名的密码