美文网首页
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