热修复也叫热更新,又叫做动态加载、动态修复、动态更新,是指不通过重新安装新的APK安装包的情况下修复一些线上的BUG。
通过这样做,可以免去发版、安装、重新打开等过程,就可以修复线上的BUG,防止用户流失。因此这是几乎每一个APP都需要的一个功能,因此很有学习的必要。
注意的是:热修复只是临时的亡羊补牢。在企业中真正的热修复发版与正式版一样,需要测试进行测试。但是热修复也存在一些兼容性问题。因此高质量的版本与热修复框架才是解决问题的最好的手段。
AndFix是阿里开源的一款热修复的框架,主要是通过底层修复的,不像dex分包中的热修复。
首先是两个已经签名的app进行对比,一个是有bug的app一个是已经修复好bug的app生成patch文件。对比工具可以到gitHua上下载。
地址:https://github.com/alibaba/AndFix
在工具里面有两个脚本文件分别是:
apkpatch.bat
apkpatch.sh
.bat结尾的是windows版本,.sh结尾的是mac和liux版本用的。
然后执行该命令:
./apkpatch.sh -f new.apk -t old.apk -o out -k nan.jks -p 123456 -a nan -e 123456
在命令里面我们执行了新旧两个APK文件,输出路径,签名文件,签名密码,签名文件的别名以及密码。
执行完命令后可以得到一个out.patch的文件,这个就是已经对比后的文件。
其实这个.patch文件就是一个jar文件,把后缀名改成.jar后可以看到就是jar包的文件目录。
通过源码可以看到是通过java去解析这个patch文件拿到要更新的类名方法名。
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private void init() {
JarFile jarFile = null;
InputStream inputStream = null;
mClassMap = new HashMap<>();
List<String> list = new ArrayList<>();
try {
jarFile = new JarFile(mFile);
JarEntry jarEntry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(jarEntry);
Manifest manifest = new Manifest(inputStream);
Attributes attributes = manifest.getMainAttributes();
Attributes.Name attrName;
for(Iterator<?> item = attributes.keySet().iterator(); item.hasNext();){
attrName = (Attributes.Name) item.next();
if(attrName != null){
String name = attrName.toString();
if(name.endsWith("Classes")){
list = Arrays.asList(attributes.getValue(name).split(","));
if(name.equalsIgnoreCase(PATCH_CLASSES)){
mClassMap.put(name,list);
}else {
mClassMap.put(name.trim().substring(0, name.length() - 8), list);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
jarFile.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
解析后的信息会存到一个Map里面,在class加载时会用到这些信息。
在patchManger这个类里面,会用到个Map里成的信息,然后传到andfixManget里面的fix方法
public void loadPathc(String path){
srcFile = new File(path);
Patch patch = new Patch(srcFile,mContext);
loadPatch(patch);
}
private void loadPatch(Patch patch){
//类加载器
ClassLoader classLoader = mContext.getClassLoader();
List<String> list;
for(String name : patch.getPatchNames()){
list = patch.getClasses(name);
mAndfixManger.fix(srcFile,classLoader,list);
}
}
上面的代码中做了一些简化。
AndFixManger中的fix方法就是把patch中的classes.dex加载到内存中
public void fix(File file, ClassLoader classLoader, List<String> list){
optFile = new File(mContext.getFilesDir(),file.getName());
if(optFile.exists()){
optFile.delete();
}
try {
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),optFile.getAbsolutePath(),Context.MODE_PRIVATE);
//這里不能用當前上下文的ClassLoader,要不然加載的還是有bug的dex文件
ClassLoader mClassLoader = new ClassLoader(){
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = dexFile.loadClass(name,this);
if(clazz == null){
clazz = Class.forName(name);
}
return clazz;
}
};
Enumeration<String> entry = dexFile.entries();
while (entry.hasMoreElements()){
String key = entry.nextElement();
if(!list.contains(key)){
continue;
}
Class realClazz=dexFile.loadClass(key,mClassLoader);
if(realClazz!=null){
fixClass(realClazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
在fix方法中,后面的while循环就是拿到patch这个类中的Map里面的信息和加载到内存的dex文件中的信息对比。如果相同那么就通过fixClazz这个方法拿到要修复的类名和方法名
private void fixClass(Class realClazz) {
Method method[] = realClazz.getMethods();
for(Method needFixMethod : method){
MethodReplace methodReplace = needFixMethod.getAnnotation(MethodReplace.class);
if(methodReplace == null){
continue;
}
Log.d(TAG,"找到替換的方法:"+methodReplace.toString()+";類對象:"+realClazz.toString());
String clazz = methodReplace.clazz();
String methodName = methodReplace.method();
Log.d(TAG,"类名:"+clazz+";方法名:"+methodName);
replaceMethod(clazz,methodName,needFixMethod);
}
}
是通过注解来确定类名和方法名的
package com.alipay.euler.andfix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {
String clazz();
String method();
}
找到要替换的方法名和类名后会调用replaceMethod方法来调用native方法来进行修复bug。
private void replaceMethod(String clazz,String metodName,Method method){
try {
Class srcClass = Class.forName(clazz);
if(srcClass!=null) {
Method srcMethod = srcClass.getDeclaredMethod(metodName, method.getParameterTypes());
Andfix.replaceMethod(srcMethod,method);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
上面已经把patch文件进行处理完了,那先要了解一下虚拟机是怎么加载方法的
devil.png
如上图所示,Android虚拟机是有别于Java原生的虚拟机的,它执行的是dex文件而不是class文件。Android虚拟机分为dalvik虚拟机和art虚拟机。
虚拟机(进程)启动的时候会加载一个很重要的动态库文件(libdalvik.so或者libart.so)。
Java在虚拟机环境中执行,每个Java方法都会对应一个底层的函数指针,当Java方法被调用的时候,实质虚拟机会找到这个函数指针然后去执行底层的方法,从而Java方法被执行。
在用native方法进行热修复时,应该会先进行初始化,具体的虚拟机注册比较复杂,为了简单起见,我们只分析一下dalvik虚拟机的初始化,具体方法如下:
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
JNIEnv* env, int apilevel) {
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
if (!dvmDecodeIndirectRef_fnPtr) {
return JNI_FALSE;
}
dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
if (!dvmThreadSelf_fnPtr) {
return JNI_FALSE;
}
jclass clazz = env->FindClass("java/lang/reflect/Method");
jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
"()Ljava/lang/Class;");
return JNI_TRUE;
} else {
return JNI_FALSE;
}
}
dalvik_setup方法主要做了两个步骤:
通过调用dlopen(该方法在系统头文件dlfcn.h中)加载libdvm.so(这个so在APP进程初始化的时候会加载),这个加载是为了下一步的Hook做准备。
加载完libdvm.so之后,就可以进行Hook了。在API10以上、以下,Java方法调用的时候会执行不同的底层的系统函数,因此必须Hook不同的系统函数才会有效。Hook成功以后,在这些系统函数调用的时候,就会调用我们自己的代码,进行替换。
我们在loadPatch的时候,最终会调用AndFixManager的fix方法,根据一系列的调用链,最终会调用dalvik_replaceMethod或者art_replaceMethod。下面继续以dalvik虚拟机为例,继续来看dalvik_replaceMethod方法的实现:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
replaceMethod函数最终就会把有bug方法结构体指针的值重新赋值到修复好的方法结构体指针中的值,从而达到修复的目的。
网友评论