美文网首页
Andfix热修复原理

Andfix热修复原理

作者: 依玲之风 | 来源:发表于2018-05-12 17:24 被阅读0次

热修复也叫热更新,又叫做动态加载、动态修复、动态更新,是指不通过重新安装新的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方法结构体指针的值重新赋值到修复好的方法结构体指针中的值,从而达到修复的目的。

相关文章

网友评论

      本文标题:Andfix热修复原理

      本文链接:https://www.haomeiwen.com/subject/hshhdftx.html