Andfix热修复原理

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容